同様の質問がMathematica.Stackexchangeに頼まれました。向こうの私の答えは進化し、最終的にはかなり長くなったので、ここでアルゴリズムを要約します。
抽象
基本的な考え方は次のとおりです。
- ラベルを見つけます。
- ラベルの境界線を見つける
- 画像座標を円柱座標にマッピングするマッピングを見つけて、ラベルの上部境界線に沿ったピクセルを([anything] / 0)に、右境界線に沿ったピクセルを(1 / [anything])などにマッピングします。
- このマッピングを使用して画像を変換します
アルゴリズムは、次の画像に対してのみ機能します。
- ラベルは背景よりも明るい(これはラベル検出に必要です)
- ラベルは長方形です(これはマッピングの品質を測定するために使用されます)
- jarは(ほぼ)垂直です(これは、マッピング機能を単純にするために使用されます)
- jarは円筒形です(これはマッピング機能をシンプルにするために使用されます)
ただし、アルゴリズムはモジュール式です。少なくとも原則として、暗い背景を必要としない独自のラベル検出を作成することも、楕円または八角形のラベルに対処できる独自の品質測定機能を作成することもできます。
結果
これらの画像は完全に自動的に処理されました。つまり、アルゴリズムはソース画像を取得し、数秒間動作し、マッピング(左)と歪みのない画像(右)を表示します。
次の画像は、アルゴリズムの修正版で処理されました。ラベルの曲率は正面ショットの画像から推定できないため、ユーザーが瓶の左右の境界線(ラベルではなく)を選択しました。完全に自動化されたアルゴリズムは、わずかに歪んだ画像を返します):
実装:
1.ラベルを見つける
ラベルは暗い背景の前で明るいため、2値化を使用して簡単に見つけることができます。
src = Import["http://i.stack.imgur.com/rfNu7.png"];
binary = FillingTransform[DeleteBorderComponents[Binarize[src]]]
最大の接続コンポーネントを選択し、それがラベルだと仮定します。
labelMask = Image[SortBy[ComponentMeasurements[binary, {"Area", "Mask"}][[All, 2]], First][[-1, 2]]]
2.ラベルの境界線を見つける
次のステップ:単純な微分畳み込みマスクを使用して、上/下/左/右の境界線を見つけます:
topBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1}, {-1}}]];
bottomBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1}, {1}}]];
leftBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1, -1}}]];
rightBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1, 1}}]];
これは、これら4つの画像のいずれかですべての白いピクセルを検出し、インデックスを座標に変換する小さなヘルパー関数です(インデックスをPosition
返します。インデックスは1ベースの{y、x}タプルで、y = 1はしかし、すべての画像処理関数は、0ベースの{x、y}タプルである座標を期待します。ここで、y = 0は画像の下部です)。
{w, h} = ImageDimensions[topBorder];
maskToPoints = Function[mask, {#[[2]]-1, h - #[[1]]+1} & /@ Position[ImageData[mask], 1.]];
3.画像から円柱座標へのマッピングを見つける
これで、ラベルの上、下、左、右の境界の座標の4つの個別のリストができました。画像座標から円柱座標へのマッピングを定義します。
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]}]]
}
これは、ソース画像のX / Y座標を円筒座標にマッピングする円筒マッピングです。マッピングには、高さ/半径/中心/遠近/傾斜の自由度が10あります。ArcSinを直接使用して最適化を行うことができなかったため、テイラー級数を使用してアークサインを近似しました。のClip
呼び出しは、最適化中に複素数を防ぐためのアドホックな試みです。ここにはトレードオフがあります:一方では、関数は可能な限り正確な円筒形のマッピングにできるだけ近く、最小限の歪みを与える必要があります。一方、複雑な場合、自由度の最適な値を自動的に見つけることははるかに難しくなります。(Mathematicaで画像処理を行うことの良い点は、このような数学モデルを簡単にいじり、さまざまな歪みの追加用語を導入し、同じ最適化関数を使用して最終結果を得ることができることです。 OpenCVやMatlabを使用するようなものです。しかし、Matlabのシンボリックツールボックスを試したことはありません。
次に、画像の品質を測定する「エラー関数」->シリンダー座標マッピングを定義します。これは、境界ピクセルの二乗誤差の合計です。
errorFunction =
Flatten[{
(mapping[#][[1]])^2 & /@ maskToPoints[leftBorder],
(mapping[#][[1]] - 1)^2 & /@ maskToPoints[rightBorder],
(mapping[#][[2]] - 1)^2 & /@ maskToPoints[topBorder],
(mapping[#][[2]])^2 & /@ maskToPoints[bottomBorder]
}];
このエラー関数は、マッピングの「品質」を測定します。左境界のポイントが(0 / [anything])にマッピングされ、上部境界のピクセルが([anything] / 0)にマッピングされる場合など、最低です。
これで、このエラー関数を最小化する係数を見つけるようにMathematicaに指示できます。いくつかの係数(たとえば、画像の瓶の半径と中心)について「教育的な推測」を行うことができます。これらを最適化の開始点として使用します。
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
エラー関数を最小化するマッピング関数の10自由度の値を見つけます。汎用マッピングとこのソリューションを組み合わせると、ラベル領域に適合するX / Y画像座標からマッピングが得られます。MathematicaのContourPlot
関数を使用して、このマッピングを視覚化できます。
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.画像を変換する
最後に、ImageForwardTransform
このマッピングに従ってMathematicaの関数を使用して画像を歪めます:
ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]
上記の結果が得られます。
手動アシストバージョン
上記のアルゴリズムは全自動です。調整は必要ありません。写真が上または下から撮影される限り、それは合理的に機能します。しかし、正面ショットの場合、瓶の半径はラベルの形状から推定できません。これらの場合、ユーザーに手動でjarの左右の境界線を入力させ、マッピングで対応する自由度を明示的に設定すると、はるかに良い結果が得られます。
このコードにより、ユーザーは左/右の境界線を選択できます。
LocatorPane[Dynamic[{{xLeft, y1}, {xRight, y2}}],
Dynamic[Show[src,
Graphics[{Red, Line[{{xLeft, 0}, {xLeft, h}}],
Line[{{xRight, 0}, {xRight, h}}]}]]]]
これは代替の最適化コードで、center&radiusが明示的に指定されています。
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]