DC娱乐网

c

c#实现扣面单功能的3种方式无论是跨境电商还是制造业分拣设备,在包裹流转出入库的场景,为了保证包裹分拣计划和测量数据绑定

c#实现扣面单功能的3种方式

无论是跨境电商还是制造业分拣设备,在包裹流转出入库的场景,为了保证包裹分拣计划和测量数据绑定真实性,经常会遇到面单扣取的需求,下面我就通过两种实现原理来实现这一功能。

一:OpenCVSharp 通过面单轮廓/颜色/边缘等组合检测实现

二:通过OCR识别面单内容,根据所有切割点坐标点最小外界矩形来定位面单位置(扣面单的场景需求是看清面单内容,当然想要扣取完整面单图片,可以添加面单尺寸,规则信息等维度计算或者直接用第三种方式)

三:YOLO+Labelme标定工具,通过模型训练定位扣取(这个抽时间单独展开一篇解释)

方式一:OpencvSharp 通过轮廓/颜色/边缘检测

这种方式对于包裹和面单颜色有明显差异的场景很友好,对于包裹颜色和面单颜色接近的效果一般(建议考虑第二种方式),虽然可以根据面单样式或者文字聚集密度等多重维度来组合分析,但是过于复杂,并且定制化程度很高,废话少说,先看看效果:

原图:

通过显示增强后的效果图:

废话少说,附上核心代码:

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{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;}}}}// 保存检测结果到JSONSaveResultsToJson(results, Path.Combine(outputDir, $"{baseName}_results.json"));Console.WriteLine("\n处理完成! 按任意键继续...");}catch (Exception ex){Console.WriteLine($"处理失败: {ex.Message}");}}

通过轮廓检测、颜色检测和边缘检测三种方式组合定位面单位置

public List DetectLabels(string imagePath){var results = new Listusing (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 DetectByContours(OpenCvSharp.Mat src){var results = new Listusing (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 _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 DetectByColor(OpenCvSharp.Mat src){var results = new Listusing (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 _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 DetectByEdges(OpenCvSharp.Mat src){var results = new Listusing (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 _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这个架构的网络模型,论文是公开的,我们在这个基础上做的复现与调优。话不多说,先看效果

相机拍照原始包裹图片

OCR识别切割效果(根据识别文字角度自动校正)

定位到每个识别内容的矩形坐标,获取所有当前图片所有切割矩形的最小外接矩形,然后裁切,就可以得到包含所有面单内容的图片

抠面单效果(实际会比面单小,但是满足客户需求,包含了所有面单内容)

废话不多说,附上代码

////// 返回面单图片//////异常信息///面单是否增强///是否本地保存///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);}// 解析矩形数据并计算最小外接矩形Listif (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;}}

图片增强显示,有需要可以调用

////// 图片增强显示//////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{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;}

结束语