OpenCvSharp基于顏色反差規避FBA面單貼標2
第一版的劣勢
原理同上一邊博客記錄,在基礎上改造的更加細致些,100*100的貼標區域,很容易讓原本就不大的FBA紙箱,留下更多空白區域,并且空白區域和原廠標簽空隙不足貼下一張新的標簽,導致東一張西一張,雖然能夠滿足規避原廠標簽的初衷,但是如果客戶需要貼多張標簽,就會捉襟見肘
解決辦法-提升精度
既然100*100的匹配,容易造成可貼標區域浪費,那么何不把精度提升到100倍呢?
把原來100*100的網格,細分為由10個10*10的網格組成,每次匹配可貼標區域,偏移一個10*10網格的網格,然后根據占用的這個10*10的網格,按照偏移的方向,向左向上分別獲取相鄰的10個網格,那不就組成了一個100*100的可貼標區域了嗎?(當然如果需要可貼標區域利用率更高,可以縮小100倍,比如1*1的網格,獲取相鄰橫向和縱向100個這樣1*1網格,也可以組成100*100的可貼標區域,本文已經把網格大小提取出來,可用作擴展配置,本文拋磚引玉,有更好的想法可以一起交流完善)。
無圖言屌,用一張粗糙的動態圖,來說明第二版本提升精度的慢動作(最下面紅色區域是硬件的物理鈑金,已經根據上篇博客當作原廠標簽標記了,所以標記為干擾區域)

先看最終效果
避免文字無趣,先看下實際的定位效果(紅色標記原廠標簽,黃色標記可貼區域坐標)

下面是模擬效果(紅色區域是人工制造的FBA原廠標簽),旋轉紙箱不同方向的貼標效果

可以看到,無論紙箱如何旋轉,新帖的標簽,都可以完全避開.
廢話少說,上源碼
![116076-20250310125032345-1793233350[1]](https://img2024.cnblogs.com/blog/116076/202509/116076-20250924180326264-1688793031.png)
大部分源碼在上個博文已經分享出來,以下附上改動點。(文章最后會附上不同紙箱的定位效果)
// 裁剪圖像(從右下角開始保留指定尺寸)
var croppedImage = AvoidFactoryLabelSDK.CropImageFromBottomRight(originalImage, boxWidthMm, boxHeightMm);
if (croppedImage.Empty())
{
Console.WriteLine("裁剪后的圖像為空");
return;
}
// 檢測所有原廠面單位置
var labelPositions = AvoidFactoryLabelSDK.DetectOriginalLabelPositions(croppedImage);
Console.WriteLine($"檢測到 {labelPositions.Count} 個原廠面單:");
foreach (var pos in labelPositions)
{
Console.WriteLine($"位置: {pos.GridCoordinate}, 尺寸: {pos.WidthMm:F1}mm × {pos.HeightMm:F1}mm");
}
// 查找可貼標簽的位置
string availablePosition = AvoidFactoryLabelSDK.FindAvailableLabelPosition(croppedImage, labelPositions);
Console.WriteLine($"可貼標簽的位置: {availablePosition}");
// 可視化結果(可選)
Bitmap resultbm = AvoidFactoryLabelSDK.VisualizeResults(croppedImage, labelPositions, availablePosition);
lblStatus.Text = availablePosition;
pictureBox1.Image = resultbm;
/// <summary>
/// 原廠標簽規避算法
/// </summary>
/// <param name="bmSource">原箱標簽</param>
/// <param name="x">返回坐標X</param>
/// <param name="y">返回坐標y</param>
/// <param name="message">異常信息</param>
/// <param name="dpi">電腦DPI</param>
/// <returns></returns>
public static Bitmap AvoidFactoryLabelAlgorithm(string imagepath, double boxWidthMm, double boxHeightMm, out int x, out int y, out string message, double sizeF = 1.7, double dpi = 300)
{
message = string.Empty;
x = y = 1;
// 加載圖像
var originalImage = Cv2.ImRead(imagepath, OpenCvSharp.ImreadModes.Grayscale);
//計算每毫米像素數 (基于300 DPI)
double PixelsPerMm = dpi / 25.4; // 約等于 11.811
// 計算面單灰度范圍
CalculateLabelGrayRange();
ShellLine.WriteLine($"計算出的面單灰度范圍: {MinLabelGray}-{MaxLabelGray}");
// 裁剪圖像(從右下角開始保留指定尺寸)
var croppedImage = CropImageFromBottomRight(originalImage, boxWidthMm, boxHeightMm);
if (croppedImage.Empty())
{
ShellLine.WriteLine("裁剪后的圖像為空");
return croppedImage.ToBitmap();
}
// 檢測所有原廠面單位置
var labelPositions = DetectOriginalLabelPositions(croppedImage);
ShellLine.WriteLine($"檢測到 {labelPositions.Count} 個原廠面單:");
foreach (var pos in labelPositions)
{
ShellLine.WriteLine($"位置: {pos.GridCoordinate}, 尺寸: {pos.WidthMm:F1}mm × {pos.HeightMm:F1}mm");
}
// 查找可貼標簽的位置
string availablePosition = FindAvailableLabelPosition(croppedImage, labelPositions);
ShellLine.WriteLine($"可貼標簽的位置: {availablePosition}");
x = availablePosition.Split('-')[0].ToIntExt();
y = availablePosition.Split('-')[1].ToIntExt();
// 可視化結果(可選)
Bitmap resultMap = VisualizeResults(croppedImage, labelPositions, availablePosition);
return resultMap;
}
// 根據顏色列表計算面單灰度范圍
public static void CalculateLabelGrayRange()
{
var grayValues = new List<int>();
foreach (var colorHex in LabelColors)
{
// 將十六進制顏色轉換為RGB
System.Drawing.Color color = ColorTranslator.FromHtml(colorHex);
// 計算灰度值 (使用標準公式: 0.299*R + 0.587*G + 0.114*B)
int grayValue = (int)(0.299 * color.R + 0.587 * color.G + 0.114 * color.B);
grayValues.Add(grayValue);
Console.WriteLine($"顏色 {colorHex} 的灰度值: {grayValue}");
}
// 計算最小和最大灰度值,并擴展范圍以容納類似顏色
MinLabelGray = grayValues.Min() - 10;
MaxLabelGray = grayValues.Max() + 10;
// 確保范圍在0-255之間
MinLabelGray = Math.Max(0, MinLabelGray);
MaxLabelGray = Math.Min(255, MaxLabelGray);
}
// 檢測所有原廠面單位置
public static List<LabelPosition> DetectOriginalLabelPositions(OpenCvSharp.Mat image)
{
var labelPositions = new List<LabelPosition>();
// 二值化圖像以分離面單區域
var binary = new OpenCvSharp.Mat();
Cv2.Threshold(image, binary, MinLabelGray, 255, ThresholdTypes.Binary);
// 形態學操作去除噪聲
var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(5, 5));
Cv2.MorphologyEx(binary, binary, MorphTypes.Open, kernel);
// 查找輪廓
Cv2.FindContours(binary, out var contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
// 過濾輪廓(按面積)
var filteredContours = contours.Where(c => Cv2.ContourArea(c) > 1000).ToList();
// 處理每個輪廓
foreach (var contour in filteredContours)
{
// 獲取輪廓的邊界矩形
var rect = Cv2.BoundingRect(contour);
// 轉換為網格坐標
string gridCoordinate = ConvertToGridCoordinate(rect, image.Rows, image.Cols);
// 計算實際尺寸(毫米)
double widthMm = rect.Width / PixelsPerMm;
double heightMm = rect.Height / PixelsPerMm;
// 添加到結果列表
labelPositions.Add(new LabelPosition
{
Rect = rect,
GridCoordinate = gridCoordinate,
WidthMm = widthMm,
HeightMm = heightMm
});
}
return labelPositions;
}
// 查找可貼標簽的位置(使用10mm×10mm基礎網格)
public static string FindAvailableLabelPosition(OpenCvSharp.Mat image, List<LabelPosition> labelPositions)
{
// 獲取圖像尺寸
int rows = image.Rows;
int cols = image.Cols;
// 計算基礎網格行列數
int baseGridCols = (int)Math.Ceiling((double)cols / BaseGridSizePixels);
int baseGridRows = (int)Math.Ceiling((double)rows / BaseGridSizePixels);
// 從右下角開始查找(先橫向,再縱向)
for (int baseRow = 0; baseRow < baseGridRows; baseRow++)
{
for (int baseCol = 0; baseCol < baseGridCols; baseCol++)
{
// 計算當前基礎網格的像素坐標(右下角)
int baseX = cols - baseCol * BaseGridSizePixels;
int baseY = rows - baseRow * BaseGridSizePixels;
// 計算100mm×100mm區域的像素坐標
int labelX = baseX - LabelSizePixels;
int labelY = baseY - LabelSizePixels;
// 檢查區域是否在圖像范圍內
if (labelX < 0 || labelY < 0)
continue;
// 創建100mm×100mm區域矩形
Rect labelRect = new Rect(labelX, labelY, LabelSizePixels, LabelSizePixels);
// 檢查區域是否與任何原廠標簽相交
bool intersects = false;
foreach (var labelPos in labelPositions)
{
if (labelRect.IntersectsWith(labelPos.Rect))
{
intersects = true;
break;
}
}
// 如果不相交,則返回當前位置
if (!intersects)
{
// 轉換為網格坐標 (baseRow+1, baseCol+1)
return $"{baseRow + 1}-{baseCol + 1}";
}
}
}
// 如果沒有找到可用位置,返回默認位置
return "1-1";
}
// 可視化結果
public static Bitmap VisualizeResults(OpenCvSharp.Mat image, List<LabelPosition> labelPositions, string availablePosition)
{
var colorImage = new OpenCvSharp.Mat();
Cv2.CvtColor(image, colorImage, ColorConversionCodes.GRAY2BGR);
int rows = image.Rows;
int cols = image.Cols;
// 繪制網格
for (int x = 0; x < cols; x += BaseGridSizePixels)
{
Cv2.Line(colorImage, new OpenCvSharp.Point(x, 0), new OpenCvSharp.Point(x, rows), Scalar.Green, 5);
}
for (int y = 0; y < rows; y += BaseGridSizePixels)
{
Cv2.Line(colorImage, new OpenCvSharp.Point(0, y), new OpenCvSharp.Point(cols, y), Scalar.Green, 5);
}
// 標記所有原廠面單位置(紅色)
foreach (var labelPos in labelPositions)
{
Cv2.Rectangle(colorImage,
labelPos.Rect.TopLeft,
labelPos.Rect.BottomRight,
Scalar.Red, 3);
// 添加標簽文本
Cv2.PutText(colorImage,
labelPos.GridCoordinate,
new OpenCvSharp.Point(labelPos.Rect.X, labelPos.Rect.Y - 5),
HersheyFonts.HersheySimplex,
0.5,
Scalar.Red,
3);
}
// 標記可貼標簽位置(黃色)
if (!string.IsNullOrEmpty(availablePosition) && availablePosition != "1-1")
{
var parts = availablePosition.Split('-');
if (parts.Length == 2)
{
int row = int.Parse(parts[0]);
int col = int.Parse(parts[1]);
// 計算100mm×100mm區域的像素坐標
int x = cols - col * BaseGridSizePixels - LabelSizePixels;
int y = rows - row * BaseGridSizePixels - LabelSizePixels;
// 確保區域在圖像范圍內
if (x < 0) x = 0;
if (y < 0) y = 0;
int width = Math.Min(LabelSizePixels, cols - x);
int height = Math.Min(LabelSizePixels, rows - y);
if (width > 0 && height > 0)
{
Cv2.Rectangle(colorImage,
new OpenCvSharp.Point(x, y),
new OpenCvSharp.Point(x + width, y + height),
Scalar.Yellow, 3);
// 添加標簽文本
Cv2.PutText(colorImage,
availablePosition,
new OpenCvSharp.Point(x + 10, y + 30),
HersheyFonts.HersheySimplex,
1,
Scalar.Yellow,
3);
}
}
}
return colorImage.ToBitmap();
}
}
demo展示效果

結束語
感謝各位耐心查閱! 如果您有更好的想法歡迎一起交流,有不懂的也可以微信公眾號聯系博主,作者公眾號會經常發一些實用的小工具和demo源碼,需要的可以去看看!另外,如果覺得本篇博文對您或者身邊朋友有幫助的,麻煩點個關注!贈人玫瑰,手留余香,您的支持就是我寫作最大的動力,感謝您的關注,期待和您一起探討!再會!

作者:Stephen-kzx
出處:http://www.rzrgm.cn/axing/
公眾號:會定時分享寫工作中或者生活中遇到的小游戲和小工具源碼。有興趣的幫忙點下關注!感恩!
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

浙公網安備 33010602011771號