直接贴代码

using System;
using System.Drawing;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;


namespace QRCodeDetectionWinForm
{
    public partial class Form1 : Form
    {
        // 私有字段:存储选中的图像文件路径
        private string _imagePath;
        // 私有字段:存储原始图像的OpenCV Mat对象(核心图像容器)
        private Mat _srcImage;

        /// <summary>
        /// 窗体构造函数:初始化窗体控件和基础配置
        /// </summary>
        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 【选择图像】按钮点击事件处理方法
        /// 功能:打开文件选择对话框,读取选中的图像并显示在原始图PictureBox中
        /// </summary>
        /// <param name="sender">事件触发源(按钮控件)</param>
        /// <param name="e">事件参数</param>
        private void btnSelectImage_Click(object sender, EventArgs e)
        {
            // 使用using语句自动释放OpenFileDialog资源,避免内存泄漏
            using (OpenFileDialog ofd = new OpenFileDialog())
            {
                // 设置文件筛选规则:只显示常见图像格式和所有文件
                ofd.Filter = "图像文件|*.jpg;*.jpeg;*.png;*.bmp|所有文件|*.*";
                // 设置对话框标题
                ofd.Title = "选择要检测的图像";

                // 如果用户选择了文件并点击"确定"
                if (ofd.ShowDialog() == DialogResult.OK)
                {
                    try
                    {
                        // 记录选中的图像路径
                        _imagePath = ofd.FileName;
                        // 读取图像:ImreadModes.Color表示以彩色模式读取(BGR通道顺序)
                        _srcImage = Cv2.ImRead(_imagePath, ImreadModes.Color);

                        // 校验图像是否读取成功(Empty()表示Mat对象为空)
                        if (_srcImage.Empty())
                        {
                            Log("错误:无法读取选中的图像文件(文件损坏/格式不支持)");
                            return;
                        }

                        // 将原始图像显示到pbxOriginal控件中
                        ShowMatInPictureBox(_srcImage, pbxOriginal);

                        // 启用【检测二维码区域】按钮(默认禁用,选图后启用)
                        btnDetect.Enabled = true;
                        // 记录日志:图像加载成功
                        Log($"成功加载图像:{_imagePath}");
                    }
                    catch (Exception ex)
                    {
                        // 捕获读取图像过程中的异常(如路径错误、权限问题)
                        Log($"加载图像失败:{ex.Message}");
                    }
                }
            }
        }

        /// <summary>
        /// 【检测二维码区域】按钮点击事件处理方法
        /// 核心功能:实现二维码区域检测的完整流程(图像处理→轮廓检测→筛选判定)
        /// </summary>
        /// <param name="sender">事件触发源(按钮控件)</param>
        /// <param name="e">事件参数</param>
        private void btnDetect_Click(object sender, EventArgs e)
        {
            // 前置校验:未加载有效图像时直接返回
            if (_srcImage == null || _srcImage.Empty())
            {
                Log("错误:未加载有效图像,请先选择图像");
                return;
            }

            try
            {
                Log("开始二维码区域检测...");
                // 强制刷新UI,避免长时间处理导致窗体假死
                Application.DoEvents();

                // ====================== 1. 声明所有图像处理所需的Mat对象 ======================
                Mat resizeImage = new Mat(); ;      // 缩放后的图像
                Mat grayImage = new Mat(); ;        // 灰度图像
                Mat equalizeHistImage = new Mat(); ;// 直方图均衡化后的图像(提升对比度)
                Mat contrastandbrightImage = new Mat(); ; // 亮度/对比度增强后的图像
                Mat thresholdImage = new Mat(); ;   // 二值化后的图像
                Mat dilateImage = new Mat(); ;      // 膨胀操作后的图像(增强轮廓连通性)
                Mat ellipseImage = new Mat(); ;     // 用于轮廓检测的临时图像(避免修改原膨胀图)


                // ====================== 2. 图像缩放 ======================
                // 此处设置为原始尺寸(Cols=宽度,Rows=高度),可根据需求调整缩放比例
                // 缩放目的:降低图像分辨率,提升处理速度;若需更高精度可改为原尺寸
                //OpenCvSharp.Size dsize = new OpenCvSharp.Size(_srcImage.Cols * 0.3, _srcImage.Rows * 0.3);
                //OpenCvSharp.Size dsize = new OpenCvSharp.Size(_srcImage.Cols * 2, _srcImage.Rows * 2);
                OpenCvSharp.Size dsize = new OpenCvSharp.Size(_srcImage.Cols, _srcImage.Rows);

                // 执行缩放:INTER_LINEAR(线性插值)是默认的高质量缩放算法
                Cv2.Resize(_srcImage, resizeImage, dsize);

                // 记录缩放后的图像尺寸
                Log($"图像缩放至:宽={resizeImage.Cols}, 高={resizeImage.Rows}");

                // ====================== 3. 转换为灰度图 ======================
                // 彩色图转灰度图:ColorConversionCodes.BGR2GRAY(OpenCV默认彩色图是BGR通道)
                // 灰度化目的:减少计算维度(单通道),简化后续处理
                Cv2.CvtColor(resizeImage, grayImage, ColorConversionCodes.BGR2GRAY);
                // 显示灰度图到pbxGray控件
                ShowMatInPictureBox(grayImage, pbxGray);

                // ====================== 4. 直方图均衡化 ======================
                // 直方图均衡化:提升图像整体对比度,使暗部细节更清晰
                // 适用场景:解决光照不均导致的图像明暗差异问题
                Cv2.EqualizeHist(grayImage, equalizeHistImage);

                // ====================== 5. 亮度/对比度增强 ======================
                // 创建与均衡化图像尺寸、类型相同的空图像(初始化全黑)
                contrastandbrightImage = Mat.Zeros(equalizeHistImage.Size(), equalizeHistImage.Type());
                // 双重循环遍历每个像素(y=行,x=列)
                for (int y = 0; y < equalizeHistImage.Rows; y++)
                {
                    for (int x = 0; x < equalizeHistImage.Cols; x++)
                    {
                        // 获取当前像素的灰度值(uchar=8位无符号整数,0-255)
                        byte pixelValue = equalizeHistImage.At<byte>(y, x);
                        // 对比度增强:像素值×6,同时用Math.Min/Max限制在0-255(避免溢出)
                        contrastandbrightImage.At<byte>(y, x) = (byte)Math.Min(255, Math.Max(0, pixelValue * 6));
                    }
                }

                // 均值模糊:3×3卷积核,平滑图像噪声,避免后续二值化产生毛刺
                Cv2.Blur(contrastandbrightImage, contrastandbrightImage, new OpenCvSharp.Size(3, 3));
                // 显示增强后的图像到pbxEnhanced控件
                ShowMatInPictureBox(contrastandbrightImage, pbxEnhanced);


                // ====================== 6. 二值化处理 ======================
                // 阈值类型:OTSU(自动计算最优阈值) + BinaryInv(反二值化)
                // 原理:OTSU算法根据图像灰度分布自动确定阈值,反二值化使目标区域为白色,背景为黑色
                // 参数说明:0=初始阈值(OTSU时无效),255=最大值,阈值类型组合
                Cv2.Threshold(contrastandbrightImage, thresholdImage, 0, 255, ThresholdTypes.Otsu | ThresholdTypes.BinaryInv);
                // 显示二值化图像到pbxBinary控件
                ShowMatInPictureBox(thresholdImage, pbxBinary);

                // ====================== 7. 膨胀操作 ======================
                // 创建结构元素:矩形3×3卷积核(膨胀的基本单元)
                Mat element = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(3, 3));
                // 执行膨胀:迭代5次,使小轮廓连通成大轮廓,便于后续检测
                // 膨胀目的:填补轮廓间隙,增强二维码区域的连通性
                Cv2.Dilate(thresholdImage, dilateImage, element, iterations: 5);
                // 显示膨胀后的图像到pbxDilate控件
                ShowMatInPictureBox(dilateImage, pbxDilate);

                // ====================== 8. 复制膨胀图用于轮廓检测 ======================
                // 复制图像:避免findContours修改原膨胀图(findContours会改变输入图像)
                dilateImage.CopyTo(ellipseImage);

                // ====================== 9. 查找轮廓 ======================
                // 声明轮廓数组:Point[][]表示多个轮廓,每个轮廓是一组点的集合
                OpenCvSharp.Point[][] twocontours;

                // 声明轮廓层级:记录轮廓的父子关系(此处未使用,但必须声明)
                HierarchyIndex[] twohierarchy;


                // 查找轮廓核心API:
                // 参数1:输入图像(二值图);参数2:输出轮廓数组;参数3:输出层级;
                // 参数4:检索模式(Tree=检索所有轮廓并建立层级);
                // 参数5:逼近模式(ApproxSimple=压缩轮廓点,如矩形只保留4个角点)
                Cv2.FindContours(ellipseImage, out twocontours, out twohierarchy, RetrievalModes.Tree, ContourApproximationModes.ApproxSimple);

                // 记录检测到的轮廓总数
                Log($"共检测到 {twocontours.Length} 个轮廓");

                // ====================== 10. 遍历轮廓并筛选二维码区域 ======================
                for (int k = 0; k < twocontours.Length; k++)
                {
                    // 第一步筛选:轮廓点数<60的视为噪声,填充为黑色(排除小轮廓)
                    if (twocontours[k].Length <= 60)
                    {
                        // DrawContours参数说明:
                        // 参数1:目标图像;参数2:轮廓数组;参数3:当前轮廓索引;
                        // 参数4:颜色(黑色);参数5:厚度(-1=填充轮廓内部)
                        Cv2.DrawContours(ellipseImage, twocontours, k, Scalar.Black, thickness: -1);
                    }
                    else
                    {
                        try
                        {
                            // 第二步筛选:拟合椭圆(判断轮廓是否接近圆形/方形,二维码定位角点特征)
                            // FitEllipse要求轮廓点数≥5,否则会抛出异常
                            RotatedRect rRect = Cv2.FitEllipse(twocontours[k]);

                            // 计算椭圆长短轴:major=长轴,minor=短轴
                            double majorAxis = Math.Max(rRect.Size.Height, rRect.Size.Width);
                            double minorAxis = Math.Min(rRect.Size.Height, rRect.Size.Width);
                            // 计算长短轴比例(越接近1,轮廓越接近正圆/正方形)
                            float rate = (float)(majorAxis / minorAxis);
                            // 记录当前轮廓的长短轴比例
                            Log($"轮廓{k}:长短轴比例={rate:F2}");

                            // 第三步筛选:长短轴比例<1.3(判定为接近正圆/正方形,符合二维码定位特征)
                            if (rate < 1.3)
                            {
                                // 计算轮廓质心(几何中心):
                                // Moments:计算轮廓的几何矩;false=不计算中心矩
                                Moments mu = Cv2.Moments(twocontours[k], false);
                                // 质心坐标公式:x = m10/m00,y = m01/m00(m00=轮廓面积,不能为0)
                                Point2f mc = new Point2f(
                                    (float)(mu.M10 / mu.M00),
                                    (float)(mu.M01 / mu.M00)
                                );

                                // 记录质心坐标(保留2位小数)
                                Log($"轮廓{k}:质心坐标=({mc.X:F2}, {mc.Y:F2})");

                                // 关键转换:Point2f(浮点型)转Point(整型),适配Circle的参数要求
                                // Math.Round:四舍五入,避免直接截断导致坐标偏移
                                OpenCvSharp.Point center = new OpenCvSharp.Point(
                                    (int)Math.Round(mc.X),
                                    (int)Math.Round(mc.Y)
                                );

                                // 绘制质心:在缩放图上画蓝色实心圆(半径10,厚度-1=填充)
                                Cv2.Circle(resizeImage, center, 10, Scalar.Blue, thickness: -1);

                                // 第四步筛选:质心是否在图像中间1/3区域(二维码通常位于图像中间)
                                bool isInXRange = mc.X > resizeImage.Cols / 3 && mc.X < 2 * resizeImage.Cols / 3;
                                bool isInYRange = mc.Y > resizeImage.Rows / 3 && mc.Y < 2 * resizeImage.Rows / 3;

                                // 满足所有筛选条件:判定为二维码区域
                                if (isInXRange && isInYRange)
                                {
                                    // 填充轮廓为蓝色(标记二维码区域)
                                    Cv2.DrawContours(resizeImage, twocontours, k, Scalar.Blue, thickness: -1);
                                    Log($"轮廓{k}:判定为二维码区域(质心在图像中间区域)");
                                }
                            }
                            else
                            {
                                // 长短轴比例≥1.3:判定为非二维码区域,填充为红色
                                Cv2.DrawContours(resizeImage, twocontours, k, Scalar.Red, thickness: -1);
                                Log($"轮廓{k}:非二维码区域(长短轴比例超过1.3)");
                            }
                        }
                        catch (OpenCvSharpException ex)
                        {
                            // 捕获拟合椭圆异常(如轮廓点数不足、轮廓非凸等)
                            Log($"轮廓{k}:拟合椭圆失败 - {ex.Message}");
                            // 跳过当前轮廓,继续处理下一个
                            continue;
                        }
                    }
                }

                // ====================== 11. 显示最终检测结果 ======================
                ShowMatInPictureBox(resizeImage, pbxResult);
                Log("二维码区域检测完成!");

                // ====================== 资源释放 ======================
                // 手动释放Mat对象(OpenCV的非托管资源,避免内存泄漏)
                resizeImage.Release();
                grayImage.Release();
                equalizeHistImage.Release();
                contrastandbrightImage.Release();
                thresholdImage.Release();
                dilateImage.Release();
                ellipseImage.Release();
            }
            catch (Exception ex)
            {
                // 捕获检测流程中的所有异常,记录详细信息
                Log($"检测过程出错:{ex.Message}");
            }
        }

        /// <summary>
        /// 辅助方法:将OpenCV的Mat图像显示到WinForm的PictureBox控件中
        /// </summary>
        /// <param name="mat">待显示的Mat图像</param>
        /// <param name="pbx">目标PictureBox控件</param>
        private void ShowMatInPictureBox(Mat mat, PictureBox pbx)
        {
            // 空图像直接返回,避免空指针异常
            if (mat.Empty()) return;

            // 使用using语句自动释放Bitmap资源:
            // BitmapConverter.ToBitmap:将Mat转换为GDI+的Bitmap对象
            using (Bitmap bmp = BitmapConverter.ToBitmap(mat))
            {
                // 新建Bitmap副本:避免原Bitmap释放后PictureBox显示空白
                pbx.Image = new Bitmap(bmp);
            }
        }

        /// <summary>
        /// 辅助方法:带时间戳的日志输出(显示到txtLog文本框)
        /// </summary>
        /// <param name="message">日志内容</param>
        private void Log(string message)
        {
            // 格式化日志:[时:分:秒] 内容 + 换行
            string logText = $"[{DateTime.Now:HH:mm:ss}] {message}{Environment.NewLine}";
            // 追加到文本框
            txtLog.AppendText(logText);
            // 自动滚动到最后一行(方便查看最新日志)
            txtLog.ScrollToCaret();
        }

        /// <summary>
        /// 窗体关闭事件:释放全局Mat资源(最后一道内存保护)
        /// </summary>
        /// <param name="e">窗体关闭事件参数</param>
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            // 调用基类的关闭逻辑
            base.OnFormClosing(e);
            // 释放原始图像的Mat资源(?. 空值保护,避免空指针)
            _srcImage?.Release();
        }
    }
}

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐