博客 / 詳情

返回

c#實現包裹扣面單的幾種方式

  無論是跨境電商還是製造業分揀設備,在包裹流轉出入庫的場景,為了保證包裹分揀計劃和測量數據綁定真實性,經常會遇到面單扣取的需求,下面我就通過兩種實現原理來實現這一功能。

    一:OpenCVSharp 通過面單輪廓/顏色/邊緣等組合檢測實現

    二:通過OCR識別面單內容,根據所有切割點座標點最小外界矩形來定位面單位置(扣面單的場景需求是看清面單內容,當然想要扣取完整面單圖片,可以添加面單尺寸,規則信息等維度計算或者直接用第三種方式)

    三:YOLO+Labelme標定工具,通過模型訓練定位扣取(這個抽時間單獨展開一篇解釋)

方式一:OpencvSharp 通過輪廓/顏色/邊緣檢測

  這種方式對於包裹和麪單顏色有明顯差異的場景很友好,對於包裹顏色和麪單顏色接近的效果一般(建議考慮第二種方式),雖然可以根據面單樣式或者文字聚集密度等多重維度來組合分析,但是過於複雜,並且定製化程度很高,廢話少説,先看看效果:

 

        原圖:

  333

 

    通過顯示增強後的效果圖:

       zsvvyv

116076-20250310125032345-1793233350[1]

廢話少説,附上核心代碼:

staticvoidProcessSingleImage(string imagePath)
{
if (!File.Exists(imagePath))
    {
        Console.WriteLine("文件不存在!");
        Console.ReadKey();
return;
    }

try
    {
        Console.WriteLine($"處理: {Path.GetFileName(imagePath)}");

var stopwatch = Stopwatch.StartNew();

// 檢測面單
var results = _detector.DetectLabels(imagePath);

        stopwatch.Stop();
        Console.WriteLine($"檢測耗時: {stopwatch.ElapsedMilliseconds}ms");
        Console.WriteLine($"找到 {results.Count} 個面單區域");

if (results.Count == 0)
        {
            Console.WriteLine("未檢測到面單!");
            Console.ReadKey();
return;
        }

// 顯示結果
foreach (var result in results)
        {
            Console.WriteLine($"- {result.DetectionMethod}: 置信度 {result.Confidence:F2}, " +
$"位置 [{result.BoundingBox.X}, {result.BoundingBox.Y}, " +
$"{result.BoundingBox.Width}, {result.BoundingBox.Height}]");
        }

// 創建輸出目錄
var outputDir = _config.OutputDirectory;
if (!Directory.Exists(outputDir))
            Directory.CreateDirectory(outputDir);

var baseName = Path.GetFileNameWithoutExtension(imagePath);

// 保存可視化結果
if (_config.SaveVisualized)
        {
using (var original = new Bitmap(imagePath))
            {
                Bitmap bitResult = ImageProcessor.DrawBoundingBoxesSafe(original, results);

var visPath = Path.Combine(outputDir, $"{baseName}_detected.png");
                ImageProcessor.SaveImage(bitResult, visPath);
                Console.WriteLine($"可視化結果已保存: {visPath}");
            }
        }

// 保存摳圖結果 
if (_config.SaveCropped)
        {
using (var mat = Cv2.ImRead(imagePath))
            {
for (int i = 0; i < results.Count; i++)
                {
var cropped = _detector.CropLabel(mat, results[i].BoundingBox);
if (cropped != null)
                    {
// 圖像增強
                        _detector.EnhanceImage(ref cropped);

var cropPath = Path.Combine(outputDir, $"{baseName}_label_{i + 1}.png");
                        Console.WriteLine(cropped);

                        ImageProcessor.SaveImage(cropped, cropPath);
                        Console.WriteLine($"摳圖已保存: {cropPath}");

                        cropped.Dispose();
                    }
                }
            }
        }

// 保存檢測結果到JSON
        SaveResultsToJson(results, Path.Combine(outputDir, $"{baseName}_results.json"));

        Console.WriteLine("\n處理完成! 按任意鍵繼續..."); 
    }
catch (Exception ex)
    {
        Console.WriteLine($"處理失敗: {ex.Message}");
    }
}

通過輪廓檢測、顏色檢測和邊緣檢測三種方式組合定位面單位置

public List<DetectionResult> DetectLabels(string imagePath)
    {
var results = new List<DetectionResult>();

using (var mat = Cv2.ImRead(imagePath, OpenCvSharp.ImreadModes.Color))
        {
if (mat.Empty())
thrownew FileNotFoundException($"無法加載圖像: {imagePath}");

// 方法1: 輪廓檢測
var contourResults = DetectByContours(mat);
            results.AddRange(contourResults);

// 方法2: 顏色檢測
var colorResults = DetectByColor(mat);
            results.AddRange(colorResults);

// 方法3: 邊緣檢測
var edgeResults = DetectByEdges(mat);
            results.AddRange(edgeResults);
        }

// 合併和篩選結果
return FilterResults(results);
    }

輪廓檢測

private List<DetectionResult> DetectByContours(OpenCvSharp.Mat src)
    {
var results = new List<DetectionResult>(); 
using (var gray = new OpenCvSharp.Mat())
using (var binary = new OpenCvSharp.Mat())
        {
            Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);

// 二值化
            Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);

// 形態學操作
var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(3, 3));
            Cv2.MorphologyEx(binary, binary, MorphTypes.Close, kernel);

// 查找輪廓
            Cv2.FindContours(binary, outvar contours, outvar hierarchy,
                RetrievalModes.External, ContourApproximationModes.ApproxSimple);

foreach (var contour in contours)
            {
var area = Cv2.ContourArea(contour); 
if (area < _minArea || area > _maxArea)
continue;

                Console.WriteLine($"面積:{area}");
var rect = Cv2.BoundingRect(contour);

// 計算寬高比
var aspectRatio = (double)rect.Width / rect.Height;

// 面單通常為矩形,寬高比在一定範圍內
if (aspectRatio > 0.5 && aspectRatio < 3.0)
                {
// 計算矩形度
var rectArea = rect.Width * rect.Height;
var rectangularity = area / rectArea;
Console.WriteLine(rectangularity);
if (rectangularity > 0.55)
                    {
                        results.Add(new DetectionResult
                        {
                            BoundingBox = rect.ToRectangle(),
                            Confidence = rectangularity,
                            DetectionMethod = "Contour"
                        });
                    }
                }
            }
        }

return results;
    }

2.顏色檢測

private List<DetectionResult> DetectByColor(OpenCvSharp.Mat src)
    {
var results = new List<DetectionResult>();

using (var hsv = new OpenCvSharp.Mat())
using (var mask = new OpenCvSharp.Mat())
        {
// 轉換到HSV色彩空間
            Cv2.CvtColor(src, hsv, ColorConversionCodes.BGR2HSV);

// 定義白色/淺色範圍
var lowerWhite1 = new Scalar(0, 0, 200);
var upperWhite1 = new Scalar(180, 30, 255);
var lowerWhite2 = new Scalar(0, 0, 180);
var upperWhite2 = new Scalar(180, 80, 255);

using (var mask1 = new OpenCvSharp.Mat())
using (var mask2 = new OpenCvSharp.Mat())
            {
                Cv2.InRange(hsv, lowerWhite1, upperWhite1, mask1);
                Cv2.InRange(hsv, lowerWhite2, upperWhite2, mask2);
                Cv2.BitwiseOr(mask1, mask2, mask);
            }

// 形態學操作
var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(5, 5));
            Cv2.MorphologyEx(mask, mask, MorphTypes.Close, kernel);
            Cv2.MorphologyEx(mask, mask, MorphTypes.Open, kernel);

// 查找輪廓
            Cv2.FindContours(mask, outvar contours, outvar hierarchy,
                RetrievalModes.External, ContourApproximationModes.ApproxSimple);

foreach (var contour in contours)
            {
var area = Cv2.ContourArea(contour);

if (area < _minArea || area > _maxArea)
continue;

var rect = Cv2.BoundingRect(contour);

// 計算顏色均勻度
var uniformity = CalculateColorUniformity(src, rect);

if (uniformity > _confidenceThreshold)
                {
                    results.Add(new DetectionResult
                    {
                        BoundingBox = rect.ToRectangle(),
                        Confidence = uniformity,
                        DetectionMethod = "Color"
                    });
                }
            }
        }

return results;
    }
3.邊緣檢測

private List<DetectionResult> DetectByEdges(OpenCvSharp.Mat src)
    {
var results = new List<DetectionResult>();

using (var gray = new OpenCvSharp.Mat())
using (var edges = new OpenCvSharp.Mat())
        {
            Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);

// 降噪
            Cv2.GaussianBlur(gray, gray, new OpenCvSharp.Size(5, 5), 1.5);

// 邊緣檢測
            Cv2.Canny(gray, edges, 50, 150);

// 膨脹
var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(3, 3));
            Cv2.Dilate(edges, edges, kernel, iterations: 2);

// 查找輪廓
            Cv2.FindContours(edges, outvar contours, outvar hierarchy,
                RetrievalModes.External, ContourApproximationModes.ApproxSimple);

foreach (var contour in contours)
            {
var area = Cv2.ContourArea(contour);

if (area < _minArea || area > _maxArea)
continue;

var rect = Cv2.BoundingRect(contour);

// 計算邊緣密度
using (var roi = new OpenCvSharp.Mat(edges, rect))
                {
var totalPixels = roi.Rows * roi.Cols;
var edgePixels = Cv2.CountNonZero(roi);
var edgeDensity = (double)edgePixels / totalPixels;

if (edgeDensity > 0.1 && rect.Width > 100 && rect.Height > 100)
                    {
                        results.Add(new DetectionResult
                        {
                            BoundingBox = rect.ToRectangle(),
                            Confidence = edgeDensity,
                            DetectionMethod = "Edge"
                        });
                    }
                }
            }
        }

return results;
    }

方式二:通過OCR識別面單內容,根據所有切割點座標點最小外界矩形來定位面單位置

    OCR基礎模型用的是SVTR-LCNet這個架構的網絡模型,論文是公開的,我們在這個基礎上做的復現與調優。話不多説,先看效果

    相機拍照原始包裹圖片

    ScreenShot_2026-01-14_185601_117

 

        OCR識別切割效果(根據識別文字角度自動校正)

  640

  定位到每個識別內容的矩形座標,獲取所有當前圖片所有切割矩形的最小外接矩形,然後裁切,就可以得到包含所有面單內容的圖片

  640 (1)

  摳面單效果(實際會比面單小,但是滿足客户需求,包含了所有面單內容)

  640 (2)

  116076-20250310125032345-1793233350[1]

  廢話不多説,附上代碼

  

///<summary>
/// 返回面單圖片
///</summary>
///<param name="errorMsg">異常信息</param>
///<param name="IsEnhanceImage">面單是否增強</param>
///<param name="IsSaveLocl">是否本地保存</param>
///<returns></returns>
public Bitmap GetLabelImageByBitmap(outstring errorMsg, bool IsEnhanceImage = true, bool IsSaveLocl = true)
 {
     Bitmap croppedImage = null;
     errorMsg = string.Empty;
     try
     {
         if (!File.Exists(imagePath))
         {
             ShellLine.WriteLine($"請確保 {imagePath} 存在");
             errorMsg = $"請確保 {imagePath} 存在";
             returnnew Bitmap(10, 10);
         }
         //圖片目錄
         string imageDir = Path.GetDirectoryName(debugImagePath);
         if (Directory.Exists(imageDir))
         {
             Directory.CreateDirectory(imageDir);
         }
         Bitmap bitmap1 = new Bitmap(imagePath);
         var rr = oCR.GetOCRDataStr(bitmap1, debugImagePath);

         // 讀取JSON文件
         string jsonFilePath = imageDir + "\\content.json";

         if (!File.Exists(jsonFilePath))
         {
             errorMsg = $"未找到JSON文件,請確保 {jsonFilePath} 存在";
             ShellLine.WriteLine($"未找到JSON文件,請確保 {jsonFilePath} 存在");
             returnnew Bitmap(imagePath);
         }

         string preRotatedImage = imageDir + "\\preRotatedImg.jpg";
         if (!File.Exists(preRotatedImage))
         {
             errorMsg = $"未找到面單文件,請確保包裹面單清晰且存在";
             ShellLine.WriteLine($"未找到面單文件,請確保包裹面單清晰且存在");
             returnnew Bitmap(imagePath);
         }

         // 解析矩形數據並計算最小外接矩形
         List<Rectangle> rectangles = ParseRectanglesFromJson(jsonFilePath);
         if (rectangles.Count == 0)
         {
             errorMsg = "未在JSON文件中找到有效的矩形數據";
             ShellLine.WriteLine("未在JSON文件中找到有效的矩形數據");
             returnnew Bitmap(imagePath);
         }

         Rectangle boundingRect = CalculateBoundingRectangle(rectangles);
         ShellLine.WriteLine($"最小外接矩形: X={boundingRect.X}, Y={boundingRect.Y}, Width={boundingRect.Width}, Height={boundingRect.Height}");
         ShellLine.WriteLine($"包含 {rectangles.Count} 個元素");

         // 加載圖片並進行裁剪
         using (Bitmap originalImage = new Bitmap(preRotatedImage))
         {
             // 確保矩形在圖片範圍內
             Rectangle safeRect = GetSafeRectangle(boundingRect, originalImage);
             // 裁剪圖片
             croppedImage = CropImage(originalImage, safeRect);
             if (IsEnhanceImage)
             {
                 // 增強顯示 
                 EnhanceImage(ref croppedImage);
             }
             if (IsSaveLocl)
             {
                 // 保存結果
                 string outputPath = Path.Combine(
                     Path.GetDirectoryName(preRotatedImage),
                     Path.GetFileNameWithoutExtension(preRotatedImage) + "_cropped_enhanced.jpg");

                 croppedImage.Save(outputPath, ImageFormat.Jpeg);
                 ShellLine.WriteLine($"處理完成!結果已保存到: {outputPath}");
             }
             // 顯示裁剪區域信息
             ShellLine.WriteLine($"\n裁剪區域信息:");
             ShellLine.WriteLine($"  原始圖片尺寸: {originalImage.Width}x{originalImage.Height}");
             ShellLine.WriteLine($"  裁剪區域: {safeRect.X}, {safeRect.Y}, {safeRect.Width}x{safeRect.Height}");
             ShellLine.WriteLine($"  增強後圖片尺寸: {croppedImage.Width}x{croppedImage.Height}");
             return croppedImage;
         }
     }
     catch (Exception ex)
     {
         errorMsg = $"處理過程中出現錯誤: {ex.Message}";
         ShellLine.WriteLine($"處理過程中出現錯誤: {ex.Message}");
         ShellLine.WriteLine($"堆棧跟蹤: {ex.StackTrace}");
         returnnew Bitmap(imagePath);
     }
     finally
     {
         // 釋放資源
         croppedImage?.Dispose();
     }
 }

  圖片增強顯示,有需要可以調用

///<summary>
/// 圖片增強顯示
///</summary>
///<param name="image"></param>
publicvoidEnhanceImage(ref Bitmap image)
 {
     using (var mat = image.ToMat())
     using (var lab = new OpenCvSharp.Mat())
     {
         // 轉換為Lab色彩空間
         Cv2.CvtColor(mat, lab, ColorConversionCodes.BGR2Lab);

         Cv2.Split(lab, outvar labChannels);

         // 對亮度通道進行直方圖均衡化
         Cv2.EqualizeHist(labChannels[0], labChannels[0]);

         Cv2.Merge(labChannels, lab);
         Cv2.CvtColor(lab, mat, ColorConversionCodes.Lab2BGR);

         // 釋放通道
         foreach (var channel in labChannels)
             channel.Dispose();

         // 更新圖像
         image.Dispose();
         image = mat.ToBitmap();
     }
 }

  獲取包含所有切割字符的最小外接矩形

// 計算包含所有矩形的最小外接矩形
  static Rectangle CalculateBoundingRectangle(List<Rectangle> rectangles)
  {
      if (rectangles.Count == 0)
          thrownew ArgumentException("矩形列表為空");

      int minX = int.MaxValue;
      int minY = int.MaxValue;
      int maxX = int.MinValue;
      int maxY = int.MinValue;

      foreach (Rectangle rect in rectangles)
      {
          minX = Math.Min(minX, rect.X);
          minY = Math.Min(minY, rect.Y);
          maxX = Math.Max(maxX, rect.X + rect.Width);
          maxY = Math.Max(maxY, rect.Y + rect.Height);
      }

      // 添加一些邊距,使裁剪更美觀
      int margin = 10;
      minX = Math.Max(0, minX - margin);
      minY = Math.Max(0, minY - margin);
      maxX = maxX + margin;
      maxY = maxY + margin;

      returnnew Rectangle(minX, minY, maxX - minX, maxY - minY);
  }

// 確保矩形在圖片範圍內
static Rectangle GetSafeRectangle(Rectangle rect, Bitmap image)
  {
      int x = Math.Max(0, Math.Min(rect.X, image.Width - 1));
      int y = Math.Max(0, Math.Min(rect.Y, image.Height - 1));
      int width = Math.Min(rect.Width, image.Width - x);
      int height = Math.Min(rect.Height, image.Height - y);

      returnnew Rectangle(x, y, width, height);
  }

// 裁剪圖片
static Bitmap CropImage(Bitmap source, Rectangle cropArea)
  {
      Bitmap target = new Bitmap(cropArea.Width, cropArea.Height);

      using (Graphics g = Graphics.FromImage(target))
      {
          g.DrawImage(source, new Rectangle(0, 0, cropArea.Width, cropArea.Height),
              cropArea, GraphicsUnit.Pixel);
      }
      return target;
  }

  結束語

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

640

 

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.