C#OpenCV识别图片二维码
直接贴代码
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();
}
}
}
更多推荐
所有评论(0)