Una domanda simile è stata posta su Mathematica.Stackexchange . La mia risposta laggiù si è evoluta e alla fine è diventata piuttosto lunga, quindi riassumo l'algoritmo qui.
Astratto
L'idea di base è:
- Trova l'etichetta.
- Trova i bordi dell'etichetta
- Trova una mappatura che associ le coordinate dell'immagine alle coordinate del cilindro in modo da mappare i pixel lungo il bordo superiore dell'etichetta su ([nulla] / 0), i pixel lungo il bordo destro su (1 / [nulla]) e così via.
- Trasforma l'immagine usando questa mappatura
L'algoritmo funziona solo per immagini in cui:
- l'etichetta è più luminosa dello sfondo (questo è necessario per il rilevamento dell'etichetta)
- l'etichetta è rettangolare (viene utilizzata per misurare la qualità di una mappatura)
- il vaso è (quasi) verticale (questo è usato per mantenere semplice la funzione di mappatura)
- il barattolo è cilindrico (serve per mantenere semplice la funzione di mappatura)
Tuttavia, l'algoritmo è modulare. Almeno in linea di principio, potresti scrivere il tuo rilevamento di etichette che non richiede uno sfondo scuro o potresti scrivere la tua funzione di misurazione della qualità in grado di far fronte a etichette ellittiche o ottagonali.
risultati
Queste immagini sono state elaborate in modo completamente automatico, ovvero l'algoritmo prende l'immagine di origine, funziona per alcuni secondi, quindi mostra la mappatura (a sinistra) e l'immagine non distorta (a destra):
Le immagini successive sono state elaborate con una versione modificata dell'algoritmo, in cui l'utente seleziona i bordi sinistro e destro del vaso (non l'etichetta), poiché la curvatura dell'etichetta non può essere stimata dall'immagine in uno scatto frontale (ovvero l'algoritmo completamente automatico restituirebbe immagini leggermente distorte):
Implementazione:
1. Trova l'etichetta
L'etichetta è luminosa di fronte a uno sfondo scuro, quindi posso trovarla facilmente usando la binarizzazione:
src = Import["http://i.stack.imgur.com/rfNu7.png"];
binary = FillingTransform[DeleteBorderComponents[Binarize[src]]]
Scelgo semplicemente il componente collegato più grande e presumo che sia l'etichetta:
labelMask = Image[SortBy[ComponentMeasurements[binary, {"Area", "Mask"}][[All, 2]], First][[-1, 2]]]
2. Trova i bordi dell'etichetta
Passaggio successivo: trova i bordi superiore / inferiore / sinistro / destro utilizzando semplici maschere di convoluzione derivata:
topBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1}, {-1}}]];
bottomBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1}, {1}}]];
leftBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1, -1}}]];
rightBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1, 1}}]];
Questa è una piccola funzione di supporto che trova tutti i pixel bianchi in una di queste quattro immagini e converte gli indici in coordinate ( Position
restituisce gli indici e gli indici sono a base {y, x} -tuple, dove y = 1 è nella parte superiore di l'immagine, ma tutte le funzioni di elaborazione delle immagini si aspettano coordinate, che sono a base {0, y} -tuple, dove y = 0 è il fondo dell'immagine):
{w, h} = ImageDimensions[topBorder];
maskToPoints = Function[mask, {#[[2]]-1, h - #[[1]]+1} & /@ Position[ImageData[mask], 1.]];
3. Trova una mappatura dall'immagine alle coordinate del cilindro
Ora ho quattro elenchi separati di coordinate dei bordi superiore, inferiore, sinistro, destro dell'etichetta. Definisco una mappatura dalle coordinate dell'immagine alle coordinate del cilindro:
arcSinSeries = Normal[Series[ArcSin[\[Alpha]], {\[Alpha], 0, 10}]]
Clear[mapping];
mapping[{x_, y_}] :=
{
c1 + c2*(arcSinSeries /. \[Alpha] -> (x - cx)/r) + c3*y + c4*x*y,
top + y*height + tilt1*Sqrt[Clip[r^2 - (x - cx)^2, {0.01, \[Infinity]}]] + tilt2*y*Sqrt[Clip[r^2 - (x - cx)^2, {0.01, \[Infinity]}]]
}
Questa è una mappatura cilindrica, che mappa le coordinate X / Y nell'immagine sorgente su coordinate cilindriche. La mappatura ha 10 gradi di libertà per altezza / raggio / centro / prospettiva / inclinazione. Ho usato la serie Taylor per approssimare il seno dell'arco, perché non riuscivo a far funzionare l'ottimizzazione direttamente con ArcSin. IlClip
le chiamate sono il mio tentativo ad-hoc di prevenire numeri complessi durante l'ottimizzazione. C'è un compromesso qui: da un lato, la funzione dovrebbe essere il più vicino possibile a una mappatura cilindrica esatta, per dare la minima distorsione possibile. D'altra parte, se è complicato, diventa molto più difficile trovare automaticamente valori ottimali per i gradi di libertà. (La cosa bella dell'elaborazione delle immagini con Mathematica è che puoi giocare con modelli matematici come questo molto facilmente, introdurre termini aggiuntivi per distorsioni diverse e utilizzare le stesse funzioni di ottimizzazione per ottenere risultati finali. Non sono mai stato in grado di fare nulla come quello usando OpenCV o Matlab. Ma non ho mai provato la cassetta degli attrezzi simbolica per Matlab, forse questo lo rende più utile.)
Successivamente definisco una "funzione di errore" che misura la qualità di un'immagine -> mappatura delle coordinate del cilindro. È solo la somma degli errori al quadrato per i pixel del bordo:
errorFunction =
Flatten[{
(mapping[#][[1]])^2 & /@ maskToPoints[leftBorder],
(mapping[#][[1]] - 1)^2 & /@ maskToPoints[rightBorder],
(mapping[#][[2]] - 1)^2 & /@ maskToPoints[topBorder],
(mapping[#][[2]])^2 & /@ maskToPoints[bottomBorder]
}];
Questa funzione di errore misura la "qualità" di una mappatura: è più bassa se i punti sul bordo sinistro sono mappati su (0 / [nulla]), i pixel sul bordo superiore sono mappati su ([nulla] / 0) e così via .
Ora posso dire a Mathematica di trovare coefficienti che minimizzano questa funzione di errore. Posso fare "ipotesi istruite" su alcuni dei coefficienti (ad esempio il raggio e il centro del vaso nell'immagine). Uso questi come punti di partenza per l'ottimizzazione:
leftMean = Mean[maskToPoints[leftBorder]][[1]];
rightMean = Mean[maskToPoints[rightBorder]][[1]];
topMean = Mean[maskToPoints[topBorder]][[2]];
bottomMean = Mean[maskToPoints[bottomBorder]][[2]];
solution =
FindMinimum[
Total[errorFunction],
{{c1, 0}, {c2, rightMean - leftMean}, {c3, 0}, {c4, 0},
{cx, (leftMean + rightMean)/2},
{top, topMean},
{r, rightMean - leftMean},
{height, bottomMean - topMean},
{tilt1, 0}, {tilt2, 0}}][[2]]
FindMinimum
trova valori per i 10 gradi di libertà della mia funzione di mappatura che minimizzano la funzione di errore. Combina la mappatura generica e questa soluzione e ottengo una mappatura dalle coordinate dell'immagine X / Y, che si adatta all'area dell'etichetta. Posso visualizzare questa mappatura usando la ContourPlot
funzione di Mathematica :
Show[src,
ContourPlot[mapping[{x, y}][[1]] /. solution, {x, 0, w}, {y, 0, h},
ContourShading -> None, ContourStyle -> Red,
Contours -> Range[0, 1, 0.1],
RegionFunction -> Function[{x, y}, 0 <= (mapping[{x, y}][[2]] /. solution) <= 1]],
ContourPlot[mapping[{x, y}][[2]] /. solution, {x, 0, w}, {y, 0, h},
ContourShading -> None, ContourStyle -> Red,
Contours -> Range[0, 1, 0.2],
RegionFunction -> Function[{x, y}, 0 <= (mapping[{x, y}][[1]] /. solution) <= 1]]]
4. Trasforma l'immagine
Infine, utilizzo la ImageForwardTransform
funzione di Mathematica per distorcere l'immagine secondo questa mappatura:
ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]
Questo dà i risultati come mostrato sopra.
Versione assistita manualmente
L'algoritmo sopra è completamente automatico. Nessuna regolazione richiesta. Funziona abbastanza bene fintanto che la foto viene scattata dall'alto o dal basso. Ma se si tratta di un colpo frontale, il raggio del barattolo non può essere stimato dalla forma dell'etichetta. In questi casi, ottengo risultati molto migliori se lascio che l'utente inserisca manualmente i bordi sinistro / destro del vaso e imposti esplicitamente i corrispondenti gradi di libertà nella mappatura.
Questo codice consente all'utente di selezionare i bordi sinistro / destro:
LocatorPane[Dynamic[{{xLeft, y1}, {xRight, y2}}],
Dynamic[Show[src,
Graphics[{Red, Line[{{xLeft, 0}, {xLeft, h}}],
Line[{{xRight, 0}, {xRight, h}}]}]]]]
Questo è il codice di ottimizzazione alternativo, in cui il centro e il raggio sono indicati esplicitamente.
manualAdjustments = {cx -> (xLeft + xRight)/2, r -> (xRight - xLeft)/2};
solution =
FindMinimum[
Total[minimize /. manualAdjustments],
{{c1, 0}, {c2, rightMean - leftMean}, {c3, 0}, {c4, 0},
{top, topMean},
{height, bottomMean - topMean},
{tilt1, 0}, {tilt2, 0}}][[2]]
solution = Join[solution, manualAdjustments]