Một câu hỏi tương tự đã được hỏi trên Mathematica.Stackexchange . Câu trả lời của tôi ở đó đã phát triển và cuối cùng khá dài, vì vậy tôi sẽ tóm tắt thuật toán ở đây.
trừu tượng
Ý tưởng cơ bản là:
- Tìm nhãn.
- Tìm đường viền của nhãn
- Tìm một ánh xạ ánh xạ tọa độ hình ảnh sang tọa độ hình trụ để nó ánh xạ các pixel dọc theo đường viền trên cùng của nhãn thành ([bất cứ thứ gì] / 0), các pixel dọc theo đường viền phải sang (1 / [bất cứ thứ gì]), v.v.
- Chuyển đổi hình ảnh bằng cách sử dụng ánh xạ này
Thuật toán chỉ hoạt động cho hình ảnh trong đó:
- nhãn sáng hơn nền (điều này là cần thiết để phát hiện nhãn)
- nhãn là hình chữ nhật (cái này được sử dụng để đo chất lượng của ánh xạ)
- jar là (gần như) dọc (điều này được sử dụng để giữ cho chức năng ánh xạ đơn giản)
- cái bình có dạng hình trụ (cái này được sử dụng để giữ cho chức năng ánh xạ đơn giản)
Tuy nhiên, thuật toán là mô-đun. Ít nhất về nguyên tắc, bạn có thể viết phát hiện nhãn của riêng bạn mà không yêu cầu nền tối hoặc bạn có thể viết hàm đo lường chất lượng của riêng bạn có thể đối phó với nhãn hình elip hoặc hình bát giác.
Các kết quả
Những hình ảnh này được xử lý hoàn toàn tự động, tức là thuật toán lấy hình ảnh nguồn, hoạt động trong vài giây, sau đó hiển thị ánh xạ (trái) và hình ảnh không bị biến dạng (phải):
Các hình ảnh tiếp theo được xử lý bằng một phiên bản sửa đổi của thuật toán, là người dùng chọn viền trái và phải của bình (không phải nhãn), vì độ cong của nhãn không thể ước tính được từ ảnh trong ảnh chụp phía trước (tức là thuật toán hoàn toàn tự động sẽ trả về hình ảnh hơi bị méo):
Thực hiện:
1. Tìm nhãn
Nhãn sáng trước một nền tối, vì vậy tôi có thể dễ dàng tìm thấy nó bằng cách sử dụng nhị phân:
src = Import["http://i.stack.imgur.com/rfNu7.png"];
binary = FillingTransform[DeleteBorderComponents[Binarize[src]]]
Tôi chỉ cần chọn thành phần được kết nối lớn nhất và cho rằng đó là nhãn:
labelMask = Image[SortBy[ComponentMeasurements[binary, {"Area", "Mask"}][[All, 2]], First][[-1, 2]]]
2. Tìm đường viền của nhãn
Bước tiếp theo: tìm đường viền trên / dưới / trái / phải bằng cách sử dụng mặt nạ chập đạo hàm đơn giản:
topBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1}, {-1}}]];
bottomBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1}, {1}}]];
leftBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1, -1}}]];
rightBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1, 1}}]];
Đây là một hàm trợ giúp nhỏ tìm thấy tất cả các pixel trắng trong một trong bốn hình ảnh này và chuyển đổi các chỉ số thành tọa độ ( Position
trả về các chỉ mục và các chỉ mục là {y, x} -tuples, trong đó y = 1 nằm ở đầu hình ảnh. Nhưng tất cả các hàm xử lý hình ảnh đều mong đợi tọa độ, dựa trên 0 x, y} -tuples, trong đó y = 0 là đáy của hình ảnh):
{w, h} = ImageDimensions[topBorder];
maskToPoints = Function[mask, {#[[2]]-1, h - #[[1]]+1} & /@ Position[ImageData[mask], 1.]];
3. Tìm ánh xạ từ hình ảnh đến tọa độ hình trụ
Bây giờ tôi có bốn danh sách tọa độ riêng biệt của các đường viền trên, dưới, trái, phải của nhãn. Tôi xác định ánh xạ từ tọa độ hình ảnh đến tọa độ hình trụ:
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]}]]
}
Đây là ánh xạ hình trụ, ánh xạ tọa độ X / Y trong ảnh nguồn thành tọa độ hình trụ. Bản đồ có 10 độ tự do cho chiều cao / bán kính / trung tâm / phối cảnh / độ nghiêng. Tôi đã sử dụng loạt Taylor để ước tính sin hình cung, vì tôi không thể trực tiếp tối ưu hóa với ArcSin. CácClip
các cuộc gọi là nỗ lực đặc biệt của tôi để ngăn chặn các số phức trong quá trình tối ưu hóa. Có một sự đánh đổi ở đây: Một mặt, chức năng phải càng gần với ánh xạ hình trụ chính xác càng tốt, để tạo ra độ méo thấp nhất có thể. Mặt khác, nếu nó phức tạp, việc tìm các giá trị tối ưu cho mức độ tự do sẽ khó hơn nhiều. (Điều thú vị khi thực hiện xử lý hình ảnh với Mathematica là bạn có thể chơi xung quanh với các mô hình toán học như thế này rất dễ dàng, giới thiệu các thuật ngữ bổ sung cho các biến dạng khác nhau và sử dụng các hàm tối ưu hóa giống nhau để có kết quả cuối cùng. Tôi chưa bao giờ có thể làm gì cả. như thế khi sử dụng OpenCV hoặc Matlab. Nhưng tôi chưa bao giờ thử hộp công cụ tượng trưng cho Matlab, có lẽ điều đó làm cho nó hữu ích hơn.)
Tiếp theo tôi xác định "hàm lỗi" đo chất lượng của hình ảnh -> ánh xạ tọa độ hình trụ. Đây chỉ là tổng số lỗi bình phương cho các pixel viền:
errorFunction =
Flatten[{
(mapping[#][[1]])^2 & /@ maskToPoints[leftBorder],
(mapping[#][[1]] - 1)^2 & /@ maskToPoints[rightBorder],
(mapping[#][[2]] - 1)^2 & /@ maskToPoints[topBorder],
(mapping[#][[2]])^2 & /@ maskToPoints[bottomBorder]
}];
Hàm lỗi này đo lường "chất lượng" của ánh xạ: Thấp nhất nếu các điểm ở viền bên trái được ánh xạ tới (0 / [bất cứ thứ gì]), các pixel ở viền trên cùng được ánh xạ tới ([bất cứ thứ gì] / 0), v.v. .
Bây giờ tôi có thể nói với Mathicala để tìm các hệ số giảm thiểu hàm lỗi này. Tôi có thể đưa ra "những phỏng đoán có giáo dục" về một số hệ số (ví dụ bán kính và tâm của bình trong ảnh). Tôi sử dụng chúng làm điểm bắt đầu của tối ưu hóa:
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
tìm các giá trị cho 10 bậc tự do của chức năng ánh xạ của tôi để giảm thiểu chức năng lỗi. Kết hợp ánh xạ chung và giải pháp này và tôi nhận được ánh xạ từ tọa độ hình ảnh X / Y, phù hợp với vùng nhãn. Tôi có thể hình dung ánh xạ này bằng ContourPlot
hàm Mathicala:
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. Chuyển đổi hình ảnh
Cuối cùng, tôi sử dụng ImageForwardTransform
chức năng của Mathicala để làm biến dạng hình ảnh theo ánh xạ này:
ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]
Điều đó cho kết quả như hình trên.
Phiên bản hỗ trợ thủ công
Thuật toán trên là hoàn toàn tự động. Không cần điều chỉnh. Nó hoạt động hợp lý tốt miễn là hình ảnh được chụp từ trên hoặc dưới. Nhưng nếu đó là ảnh chụp phía trước, bán kính của bình không thể ước tính được từ hình dạng của nhãn. Trong những trường hợp này, tôi nhận được kết quả tốt hơn nhiều nếu tôi cho phép người dùng nhập các đường viền trái / phải của bình theo cách thủ công và đặt mức độ tự do tương ứng trong ánh xạ một cách rõ ràng.
Mã này cho phép người dùng chọn viền trái / phải:
LocatorPane[Dynamic[{{xLeft, y1}, {xRight, y2}}],
Dynamic[Show[src,
Graphics[{Red, Line[{{xLeft, 0}, {xLeft, h}}],
Line[{{xRight, 0}, {xRight, h}}]}]]]]
Đây là mã tối ưu hóa thay thế, trong đó trung tâm & bán kính được cung cấp rõ ràng.
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]