找回密码
 立即注册
首页 业界区 业界 OpenCvSharp基于颜色反差规避FBA面单贴标2

OpenCvSharp基于颜色反差规避FBA面单贴标2

柏雅云 2025-9-25 10:47:38
第一版的劣势
        原理同上一边博客记录,在基础上改造的更加细致些,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的可贴标区域,本文已经把网格大小提取出来,可用作扩展配置,本文抛砖引玉,有更好的想法可以一起交流完善)。
      无图言屌,用一张粗糙的动态图,来说明第二版本提升精度的慢动作(最下面红色区域是硬件的物理钣金,已经根据上篇博客当作原厂标签标记了,所以标记为干扰区域)
1.gif

先看最终效果
避免文字无趣,先看下实际的定位效果(红色标记原厂标签,黄色标记可贴区域坐标)
2.jpeg

下面是模拟效果(红色区域是人工制造的FBA原厂标签),旋转纸箱不同方向的贴标效果
3.png

可以看到,无论纸箱如何旋转,新帖的标签,都可以完全避开.
废话少说,上源码
4.png

大部分源码在上个博文已经分享出来,以下附上改动点。(文章最后会附上不同纸箱的定位效果)
  1.    // 裁剪图像(从右下角开始保留指定尺寸)
  2.    var croppedImage = AvoidFactoryLabelSDK.CropImageFromBottomRight(originalImage, boxWidthMm, boxHeightMm);
  3.    if (croppedImage.Empty())
  4.    {
  5.        Console.WriteLine("裁剪后的图像为空");
  6.        return;
  7.    }
  8.    // 检测所有原厂面单位置
  9.    var labelPositions = AvoidFactoryLabelSDK.DetectOriginalLabelPositions(croppedImage);
  10.    Console.WriteLine($"检测到 {labelPositions.Count} 个原厂面单:");
  11.    foreach (var pos in labelPositions)
  12.    {
  13.        Console.WriteLine($"位置: {pos.GridCoordinate}, 尺寸: {pos.WidthMm:F1}mm × {pos.HeightMm:F1}mm");
  14.    }
  15.    // 查找可贴标签的位置
  16.    string availablePosition = AvoidFactoryLabelSDK.FindAvailableLabelPosition(croppedImage, labelPositions);
  17.    Console.WriteLine($"可贴标签的位置: {availablePosition}");
  18.    // 可视化结果(可选)
  19.    Bitmap resultbm = AvoidFactoryLabelSDK.VisualizeResults(croppedImage, labelPositions, availablePosition);
  20.    lblStatus.Text = availablePosition;
  21.    pictureBox1.Image = resultbm;
复制代码
  1. /// <summary>
  2. /// 原厂标签规避算法
  3. /// </summary>
  4. /// <param name="bmSource">原箱标签</param>
  5. /// <param name="x">返回坐标X</param>
  6. /// <param name="y">返回坐标y</param>
  7. /// <param name="message">异常信息</param>
  8. /// <param name="dpi">电脑DPI</param>
  9. /// <returns></returns>
  10. 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)
  11. {
  12.      message = string.Empty;
  13.      x = y = 1;
  14.      // 加载图像
  15.      var originalImage = Cv2.ImRead(imagepath, OpenCvSharp.ImreadModes.Grayscale);
  16.      //计算每毫米像素数 (基于300 DPI)
  17.      double PixelsPerMm = dpi / 25.4; // 约等于 11.811
  18.                                       // 计算面单灰度范围
  19.      CalculateLabelGrayRange();
  20.      ShellLine.WriteLine($"计算出的面单灰度范围: {MinLabelGray}-{MaxLabelGray}");
  21.      // 裁剪图像(从右下角开始保留指定尺寸)
  22.      var croppedImage = CropImageFromBottomRight(originalImage, boxWidthMm, boxHeightMm);
  23.      if (croppedImage.Empty())
  24.      {
  25.          ShellLine.WriteLine("裁剪后的图像为空");
  26.          return croppedImage.ToBitmap();
  27.      }
  28.       
  29.      // 检测所有原厂面单位置
  30.      var labelPositions = DetectOriginalLabelPositions(croppedImage);
  31.      ShellLine.WriteLine($"检测到 {labelPositions.Count} 个原厂面单:");
  32.      foreach (var pos in labelPositions)
  33.      {
  34.          ShellLine.WriteLine($"位置: {pos.GridCoordinate}, 尺寸: {pos.WidthMm:F1}mm × {pos.HeightMm:F1}mm");
  35.      }
  36.      // 查找可贴标签的位置
  37.      string availablePosition = FindAvailableLabelPosition(croppedImage, labelPositions);
  38.      ShellLine.WriteLine($"可贴标签的位置: {availablePosition}");
  39.      x = availablePosition.Split('-')[0].ToIntExt();
  40.      y = availablePosition.Split('-')[1].ToIntExt();
  41.      // 可视化结果(可选)
  42.      Bitmap resultMap =  VisualizeResults(croppedImage, labelPositions, availablePosition);
  43.      
  44.      return resultMap;
  45. }
复制代码
  
  1.   // 根据颜色列表计算面单灰度范围
  2.   public static void CalculateLabelGrayRange()
  3.   {
  4.       var grayValues = new List<int>();
  5.       foreach (var colorHex in LabelColors)
  6.       {
  7.           // 将十六进制颜色转换为RGB
  8.           System.Drawing.Color color = ColorTranslator.FromHtml(colorHex);
  9.           // 计算灰度值 (使用标准公式: 0.299*R + 0.587*G + 0.114*B)
  10.           int grayValue = (int)(0.299 * color.R + 0.587 * color.G + 0.114 * color.B);
  11.           grayValues.Add(grayValue);
  12.           Console.WriteLine($"颜色 {colorHex} 的灰度值: {grayValue}");
  13.       }
  14.       // 计算最小和最大灰度值,并扩展范围以容纳类似颜色
  15.       MinLabelGray = grayValues.Min() - 10;
  16.       MaxLabelGray = grayValues.Max() + 10;
  17.       // 确保范围在0-255之间
  18.       MinLabelGray = Math.Max(0, MinLabelGray);
  19.       MaxLabelGray = Math.Min(255, MaxLabelGray);
  20.   }
复制代码
  
  1. // 检测所有原厂面单位置
  2. public static List<LabelPosition> DetectOriginalLabelPositions(OpenCvSharp.Mat image)
  3. {
  4.      var labelPositions = new List<LabelPosition>();
  5.      // 二值化图像以分离面单区域
  6.      var binary = new OpenCvSharp.Mat();
  7.      Cv2.Threshold(image, binary, MinLabelGray, 255, ThresholdTypes.Binary);
  8.      // 形态学操作去除噪声
  9.      var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(5, 5));
  10.      Cv2.MorphologyEx(binary, binary, MorphTypes.Open, kernel);
  11.      // 查找轮廓
  12.      Cv2.FindContours(binary, out var contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
  13.      // 过滤轮廓(按面积)
  14.      var filteredContours = contours.Where(c => Cv2.ContourArea(c) > 1000).ToList();
  15.      // 处理每个轮廓
  16.      foreach (var contour in filteredContours)
  17.      {
  18.          // 获取轮廓的边界矩形
  19.          var rect = Cv2.BoundingRect(contour);
  20.          // 转换为网格坐标
  21.          string gridCoordinate = ConvertToGridCoordinate(rect, image.Rows, image.Cols);
  22.          // 计算实际尺寸(毫米)
  23.          double widthMm = rect.Width / PixelsPerMm;
  24.          double heightMm = rect.Height / PixelsPerMm;
  25.          // 添加到结果列表
  26.          labelPositions.Add(new LabelPosition
  27.          {
  28.              Rect = rect,
  29.              GridCoordinate = gridCoordinate,
  30.              WidthMm = widthMm,
  31.              HeightMm = heightMm
  32.          });
  33.      }
  34.      return labelPositions;
  35. }
复制代码
  
  1.    // 查找可贴标签的位置(使用10mm×10mm基础网格)
  2.    public static string FindAvailableLabelPosition(OpenCvSharp.Mat image, List<LabelPosition> labelPositions)
  3.    {
  4.        // 获取图像尺寸
  5.        int rows = image.Rows;
  6.        int cols = image.Cols;
  7.        // 计算基础网格行列数
  8.        int baseGridCols = (int)Math.Ceiling((double)cols / BaseGridSizePixels);
  9.        int baseGridRows = (int)Math.Ceiling((double)rows / BaseGridSizePixels);
  10.        // 从右下角开始查找(先横向,再纵向)
  11.        for (int baseRow = 0; baseRow < baseGridRows; baseRow++)
  12.        {
  13.            for (int baseCol = 0; baseCol < baseGridCols; baseCol++)
  14.            {
  15.                // 计算当前基础网格的像素坐标(右下角)
  16.                int baseX = cols - baseCol * BaseGridSizePixels;
  17.                int baseY = rows - baseRow * BaseGridSizePixels;
  18.                // 计算100mm×100mm区域的像素坐标
  19.                int labelX = baseX - LabelSizePixels;
  20.                int labelY = baseY - LabelSizePixels;
  21.                // 检查区域是否在图像范围内
  22.                if (labelX < 0 || labelY < 0)
  23.                    continue;
  24.                // 创建100mm×100mm区域矩形
  25.                Rect labelRect = new Rect(labelX, labelY, LabelSizePixels, LabelSizePixels);
  26.                // 检查区域是否与任何原厂标签相交
  27.                bool intersects = false;
  28.                foreach (var labelPos in labelPositions)
  29.                {
  30.                    if (labelRect.IntersectsWith(labelPos.Rect))
  31.                    {
  32.                        intersects = true;
  33.                        break;
  34.                    }
  35.                }
  36.                // 如果不相交,则返回当前位置
  37.                if (!intersects)
  38.                {
  39.                    // 转换为网格坐标 (baseRow+1, baseCol+1)
  40.                    return $"{baseRow + 1}-{baseCol + 1}";
  41.                }
  42.            }
  43.        }
  44.        // 如果没有找到可用位置,返回默认位置
  45.        return "1-1";
  46.    }
复制代码
  
  1.     // 可视化结果
  2.     public static Bitmap VisualizeResults(OpenCvSharp.Mat image, List<LabelPosition> labelPositions, string availablePosition)
  3.     {
  4.         var colorImage = new OpenCvSharp.Mat();
  5.         Cv2.CvtColor(image, colorImage, ColorConversionCodes.GRAY2BGR);
  6.         int rows = image.Rows;
  7.         int cols = image.Cols;
  8.         // 绘制网格
  9.         for (int x = 0; x < cols; x += BaseGridSizePixels)
  10.         {
  11.             Cv2.Line(colorImage, new OpenCvSharp.Point(x, 0), new OpenCvSharp.Point(x, rows), Scalar.Green, 5);
  12.         }
  13.         for (int y = 0; y < rows; y += BaseGridSizePixels)
  14.         {
  15.             Cv2.Line(colorImage, new OpenCvSharp.Point(0, y), new OpenCvSharp.Point(cols, y), Scalar.Green, 5);
  16.         }
  17.         // 标记所有原厂面单位置(红色)
  18.         foreach (var labelPos in labelPositions)
  19.         {
  20.             Cv2.Rectangle(colorImage,
  21.                          labelPos.Rect.TopLeft,
  22.                          labelPos.Rect.BottomRight,
  23.                          Scalar.Red, 3);
  24.             // 添加标签文本
  25.             Cv2.PutText(colorImage,
  26.                        labelPos.GridCoordinate,
  27.                        new OpenCvSharp.Point(labelPos.Rect.X, labelPos.Rect.Y - 5),
  28.                        HersheyFonts.HersheySimplex,
  29.                        0.5,
  30.                        Scalar.Red,
  31.                        3);
  32.         }
  33.         // 标记可贴标签位置(黄色)
  34.         if (!string.IsNullOrEmpty(availablePosition) && availablePosition != "1-1")
  35.         {
  36.             var parts = availablePosition.Split('-');
  37.             if (parts.Length == 2)
  38.             {
  39.                 int row = int.Parse(parts[0]);
  40.                 int col = int.Parse(parts[1]);
  41.                 // 计算100mm×100mm区域的像素坐标
  42.                 int x = cols - col * BaseGridSizePixels - LabelSizePixels;
  43.                 int y = rows - row * BaseGridSizePixels - LabelSizePixels;
  44.                 // 确保区域在图像范围内
  45.                 if (x < 0) x = 0;
  46.                 if (y < 0) y = 0;
  47.                 int width = Math.Min(LabelSizePixels, cols - x);
  48.                 int height = Math.Min(LabelSizePixels, rows - y);
  49.                 if (width > 0 && height > 0)
  50.                 {
  51.                     Cv2.Rectangle(colorImage,
  52.                                  new OpenCvSharp.Point(x, y),
  53.                                  new OpenCvSharp.Point(x + width, y + height),
  54.                                  Scalar.Yellow, 3);
  55.                     // 添加标签文本
  56.                     Cv2.PutText(colorImage,
  57.                                availablePosition,
  58.                                new OpenCvSharp.Point(x + 10, y + 30),
  59.                                HersheyFonts.HersheySimplex,
  60.                                1,
  61.                                Scalar.Yellow,
  62.                                3);
  63.                 }
  64.             }
  65.         }
  66.         return colorImage.ToBitmap();
  67.     }
  68. }
复制代码
  demo展示效果
5.gif

结束语
        感谢各位耐心查阅!  如果您有更好的想法欢迎一起交流,有不懂的也可以微信公众号联系博主,作者公众号会经常发一些实用的小工具和demo源码,需要的可以去看看!另外,如果觉得本篇博文对您或者身边朋友有帮助的,麻烦点个关注!赠人玫瑰,手留余香,您的支持就是我写作最大的动力,感谢您的关注,期待和您一起探讨!再会!
6.gif

 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册