OpenCvSharp基于顏色反差規避FBA面單貼標
01
規避原理
1.摳圖,根據色差或者根據固定包裹位置以及包裹尺寸摳出紙箱圖片
2.色差,獲取紙箱上所有背景色的灰度值
3.采圖,采集大量視野相同,光源相同面單的色差灰度值,整理區間
4.取反,所有非面單灰度值區間的,都認為是紙箱背景色
02-
根據DPI計算1mm對應像素點。
-
獲取吸取的顏色,計算灰度值
// 300 DPI 計算:每毫米像素數 = 300 / 25.4 ≈ 11.81
private const double PixelsPerMm = 300.0 / 25.4 ;
private static int LabelSizePixels = Convert.ToInt32(Math.Ceiling((int)(95 * PixelsPerMm) / 1.7)); // 100mm × 100mm
// 面單顏色列表(十六進制格式)
private static readonly List<string> LabelColors = new List<string>
{
"#E2E2E0", "#DEDEDC", "#E0E0DE", "#CCCCCA", "#B2B2B0", "#C2C2C0","#FFFFFF","#FEFEFE","#FCFCFC" ,"#ADADAD"
};
// 計算出的面單灰度范圍
private static int MinLabelGray;
private static int MaxLabelGray;
// 加載圖像
var originalImage = Cv2.ImRead(@"D:\Users\steph\Pictures\1分4\Image_20250913210539498.jpg", OpenCvSharp.ImreadModes.Grayscale);
// 計算面單灰度范圍
CalculateLabelGrayRange();
Console.WriteLine($"計算出的面單灰度范圍: {MinLabelGray}-{MaxLabelGray}");
if (originalImage.Empty())
{
Console.WriteLine("無法加載圖像");
return;
}
此處用第二種最簡單方式演示,視野固定包裹位置,根據計算包裹的尺寸,扣除原箱外觀
// 從右下角裁剪圖像
private static OpenCvSharp.Mat CropImageFromBottomRight(OpenCvSharp.Mat image, double widthMm, double heightMm)
{
// 將毫米轉換為像素
int widthPixels = (int)(widthMm * PixelsPerMm/1.7);
int heightPixels = (int)(heightMm * PixelsPerMm/1.7);
// 獲取圖像尺寸
int imgWidth = image.Cols;
int imgHeight = image.Rows;
// 計算裁剪區域的左上角坐標
int x = Math.Max(0, imgWidth - widthPixels);
int y = Math.Max(0, imgHeight - heightPixels);
// 確保裁剪區域不超出圖像邊界
widthPixels = Math.Min(widthPixels, imgWidth - x);
heightPixels = Math.Min(heightPixels, imgHeight - y);
// 檢查裁剪區域是否有效
if (widthPixels <= 0 || heightPixels <= 0)
{
Console.WriteLine($"無效的裁剪區域: x={x}, y={y}, width={widthPixels}, height={heightPixels}");
return image.Clone(); // 返回原始圖像的副本
}
Console.WriteLine($"裁剪區域: x={x}, y={y}, width={widthPixels}, height={heightPixels}");
// 裁剪圖像
return new OpenCvSharp.Mat(image, new Rect(x, y, widthPixels, heightPixels));
}
根據裁切后的原箱外觀,以及灰度值區間,定位原廠面單位置
// 檢測所有原廠面單位置
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;
}
檢查可貼標簽位置是否與原廠標簽有交集,檢查可貼區域是否超過原箱尺寸,此處我們以新帖面單大小100mm*100mm為例。沒有可貼標簽位置默認選擇1-1位置貼標
// 查找可貼標簽的位置(與原廠標簽無交集的網格)
public static List<string> FindAvailableLabelPositions(OpenCvSharp.Mat image, List<LabelPosition> labelPositions)
{
var availablePositions = new List<string>();
// 獲取圖像尺寸
int rows = image.Rows;
int cols = image.Cols;
// 計算網格行列數
int gridCols = (int)Math.Ceiling((double)cols / LabelSizePixels);
int gridRows = (int)Math.Ceiling((double)rows / LabelSizePixels);
// 檢查每個網格位置是否可用
for (int row = 1; row <= gridRows; row++)
{
for (int col = 1; col <= gridCols; col++)
{
// 計算當前網格的像素坐標
int x1 = Math.Max(0, cols - col * LabelSizePixels);
int y1 = Math.Max(0, rows - row * LabelSizePixels);
int x2 = Math.Min(cols, x1 + LabelSizePixels);
int y2 = Math.Min(rows, y1 + LabelSizePixels);
// 確保區域足夠大
if (x2 - x1 < LabelSizePixels / 2 || y2 - y1 < LabelSizePixels / 2)
continue;
// 創建網格矩形
Rect gridRect = new Rect(x1, y1, x2 - x1, y2 - y1);
// 檢查網格是否與任何原廠標簽相交
bool intersects = false;
foreach (var labelPos in labelPositions)
{
if (gridRect.IntersectsWith(labelPos.Rect))
{
intersects = true;
break;
}
}
// 如果不相交且區域顏色均勻,則添加到可用位置
if (!intersects && IsAreaUniform(image, x1, y1, x2, y2))
{
availablePositions.Add($"{row}-{col}");
}
}
}
return availablePositions;
}
可視化結果,以綠色網格鋪滿原箱,以紅色區域標定原廠標簽位置,以藍色網格標定可貼標簽位置,返回可視化結果
// 可視化結果
public static OpenCvSharp.Mat VisualizeResults(OpenCvSharp.Mat image, List<LabelPosition> labelPositions, List<string> availablePositions)
{
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 += LabelSizePixels)
{
Cv2.Line(colorImage, new OpenCvSharp.Point(x, 0), new OpenCvSharp.Point(x, rows), Scalar.Green, 2);
}
for (int y = 0; y < rows; y += LabelSizePixels)
{
Cv2.Line(colorImage, new OpenCvSharp.Point(0, y), new OpenCvSharp.Point(cols, y), Scalar.Green, 2);
}
// 標記所有原廠面單位置(紅色)
foreach (var labelPos in labelPositions)
{
Cv2.Rectangle(colorImage,
labelPos.Rect.TopLeft,
labelPos.Rect.BottomRight,
Scalar.Red, 2);
// 添加標簽文本
Cv2.PutText(colorImage,
labelPos.GridCoordinate,
new OpenCvSharp.Point(labelPos.Rect.X, labelPos.Rect.Y - 5),
HersheyFonts.HersheySimplex,
0.5,
Scalar.Red,
2);
}
// 標記可貼標簽位置(藍色)
foreach (var pos in availablePositions)
{
var parts = pos.Split('-');
if (parts.Length != 2) continue;
int row = int.Parse(parts[0]);
int col = int.Parse(parts[1]);
int x = Math.Max(0, cols - col * LabelSizePixels);
int y = Math.Max(0, rows - row * LabelSizePixels);
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.Blue, 2);
// 添加標簽文本
Cv2.PutText(colorImage,
pos,
new OpenCvSharp.Point(x + 5, y + 15),
HersheyFonts.HersheySimplex,
0.4,
Scalar.Blue,
1);
}
}
return colorImage;
}
看看效果1,運行看看效果.(如下圖紙箱長400,高190)白色原廠面單占據了前面6個網格,最后兩個網格超過原箱尺寸無效,默認返回第一個網格(視情況自定義)

保持期待 奔赴山海KEEP LOOKING FORWARD TO GOING
效果2.原廠標簽占據第一個和中間4個網格,可貼標簽區域藍色網格標識,并返回可貼坐標

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

浙公網安備 33010602011771號