编辑
2025-02-12
C# 应用
00
请注意,本文编写于 115 天前,最后修改于 115 天前,其中某些信息可能已经过时。

目录

功能特点
核心实现
1. 必要的类引用
2. 关键字段定义
3. 事件处理器设置
4. 圆形区域判定
5. 坐标转换计算
6. 圆形区域截取
注意事项
完整代码获取

本文将详细介绍如何在 WinForms 应用中实现图片的圆形区域选择和截取功能,使用 OpenCvSharp 进行图像处理。

功能特点

  1. 加载图片并显示
  2. 交互式圆形区域选择
  3. 可拖动调整圆形位置
  4. 实时预览选择区域
  5. 截取选中的圆形区域
  6. 保持图片原始比例显示

核心实现

image.png

1. 必要的类引用

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

2. 关键字段定义

C#
private Bitmap sourceBitmap; // 源图片 private Point startPoint; // 圆心位置 private Point currentPoint; // 当前鼠标位置 private Point lastMousePosition; // 上次鼠标位置 private bool isDrawing = false; // 是否正在绘制 private bool isMoving = false; // 是否正在移动 private int radius = 0; // 圆形半径 private const int HitTestTolerance = 1; // 点击判定容差

3. 事件处理器设置

C#
private void SetupEventHandlers() { pictureBox1.MouseDown += PictureBox1_MouseDown; pictureBox1.MouseMove += PictureBox1_MouseMove; pictureBox1.MouseUp += PictureBox1_MouseUp; pictureBox1.Paint += PictureBox1_Paint; btnCapture.Click += CaptureButton_Click; // 设置双缓冲减少闪烁 typeof(PictureBox).InvokeMember("DoubleBuffered", BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.NonPublic, null, pictureBox1, new object[] { true }); }

4. 圆形区域判定

C#
private bool IsPointNearCircle(Point point) { if (radius == 0) return false; double distance = Math.Sqrt( Math.Pow(point.X - startPoint.X, 2) + Math.Pow(point.Y - startPoint.Y, 2)); return Math.Abs(distance - radius) < HitTestTolerance; } private bool IsPointInsideCircle(Point point) { if (radius == 0) return false; double distance = Math.Sqrt( Math.Pow(point.X - startPoint.X, 2) + Math.Pow(point.Y - startPoint.Y, 2)); return distance < radius; }

5. 坐标转换计算

C#
private (float scaleX, float scaleY, float offsetX, float offsetY) GetImageScaleAndOffset() { if (sourceBitmap == null || pictureBox1.Image == null) return (1, 1, 0, 0); float containerRatio = (float)pictureBox1.Width / pictureBox1.Height; float imageRatio = (float)sourceBitmap.Width / sourceBitmap.Height; float scaleX, scaleY, offsetX = 0, offsetY = 0; if (containerRatio > imageRatio) { scaleY = (float)sourceBitmap.Height / pictureBox1.Height; scaleX = scaleY; offsetX = (pictureBox1.Width - (sourceBitmap.Width / scaleX)) / 2; } else { scaleX = (float)sourceBitmap.Width / pictureBox1.Width; scaleY = scaleX; offsetY = (pictureBox1.Height - (sourceBitmap.Height / scaleY)) / 2; } return (scaleX, scaleY, offsetX, offsetY); }

6. 圆形区域截取

C#
private void CaptureButton_Click(object sender, EventArgs e) { if (sourceBitmap == null || radius == 0) return; try { using (Mat sourceMat = BitmapConverter.ToMat(sourceBitmap)) using (Mat mask = new Mat(sourceMat.Size(), MatType.CV_8UC1, Scalar.Black)) using (Mat result = new Mat(sourceMat.Size(), sourceMat.Type(), Scalar.Black)) { var (scaleX, scaleY, offsetX, offsetY) = GetImageScaleAndOffset(); int actualX = (int)((startPoint.X - offsetX) * scaleX); int actualY = (int)((startPoint.Y - offsetY) * scaleY); int actualRadius = (int)(radius * scaleX); Cv2.Circle(mask, new OpenCvSharp.Point(actualX, actualY), actualRadius, Scalar.White, -1); sourceMat.CopyTo(result, mask); OpenCvSharp.Rect roi = new OpenCvSharp.Rect( actualX - actualRadius, actualY - actualRadius, actualRadius * 2, actualRadius * 2); // 确保ROI在图像范围内 roi.X = Math.Max(0, roi.X); roi.Y = Math.Max(0, roi.Y); roi.Width = Math.Min(result.Width - roi.X, roi.Width); roi.Height = Math.Min(result.Height - roi.Y, roi.Height); if (roi.Width > 0 && roi.Height > 0) { using (Mat cropped = new Mat(result, roi)) { pictureBox2.Image?.Dispose(); pictureBox2.Image = BitmapConverter.ToBitmap(cropped); } } } } catch (Exception ex) { MessageBox.Show($"截取图片时出错:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } }

注意事项

  1. 图片显示时保持原始比例,避免变形,最好用Zoom模式。
  2. 使用双缓冲绘制,减少闪烁
  3. 正确处理图片资源的释放
  4. 考虑图片缩放和偏移的精确计算
  5. 确保截取区域在图片范围内

完整代码获取

C#
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using OpenCvSharp; using OpenCvSharp.Extensions; using Point = System.Drawing.Point; using Timer = System.Windows.Forms.Timer; namespace WinFormsApp1 { public partial class Form6 : Form { private Bitmap sourceBitmap; private Point startPoint; private Point currentPoint; private Point lastMousePosition; private bool isDrawing = false; private bool isMoving = false; private int radius = 0; // 用于判断是否点击在圆上的容差值(像素) private const int HitTestTolerance = 1; public Form6() { InitializeComponent(); SetupEventHandlers(); } private void SetupEventHandlers() { pictureBox1.MouseDown += PictureBox1_MouseDown; pictureBox1.MouseMove += PictureBox1_MouseMove; pictureBox1.MouseUp += PictureBox1_MouseUp; pictureBox1.Paint += PictureBox1_Paint; btnCapture.Click += CaptureButton_Click; // 设置双缓冲,减少闪烁 typeof(PictureBox).InvokeMember("DoubleBuffered", BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.NonPublic, null, pictureBox1, new object[] { true }); } private bool IsPointNearCircle(Point point) { if (radius == 0) return false; // 计算点击位置到圆心的距离 double distance = Math.Sqrt( Math.Pow(point.X - startPoint.X, 2) + Math.Pow(point.Y - startPoint.Y, 2)); // 判断点击位置是否在圆的边界附近 return Math.Abs(distance - radius) < HitTestTolerance; } private bool IsPointInsideCircle(Point point) { if (radius == 0) return false; // 计算点击位置到圆心的距离 double distance = Math.Sqrt( Math.Pow(point.X - startPoint.X, 2) + Math.Pow(point.Y - startPoint.Y, 2)); // 判断点击位置是否在圆内 return distance < radius; } private void PictureBox1_MouseDown(object sender, MouseEventArgs e) { if (sourceBitmap == null) return; if (e.Button == MouseButtons.Left) { // 检查是否点击在已有的圆上或圆内 if (radius > 0 && (IsPointNearCircle(e.Location) || IsPointInsideCircle(e.Location))) { isMoving = true; lastMousePosition = e.Location; pictureBox1.Cursor = Cursors.SizeAll; } else { isDrawing = true; startPoint = e.Location; currentPoint = e.Location; radius = 0; } pictureBox1.Invalidate(); } } private void PictureBox1_MouseMove(object sender, MouseEventArgs e) { if (sourceBitmap == null) return; if (isDrawing) { currentPoint = e.Location; radius = (int)Math.Sqrt( Math.Pow(currentPoint.X - startPoint.X, 2) + Math.Pow(currentPoint.Y - startPoint.Y, 2)); pictureBox1.Invalidate(); } else if (isMoving) { // 计算移动的距离 int deltaX = e.X - lastMousePosition.X; int deltaY = e.Y - lastMousePosition.Y; // 更新圆心位置 startPoint = new Point(startPoint.X + deltaX, startPoint.Y + deltaY); // 更新上一次鼠标位置 lastMousePosition = e.Location; pictureBox1.Invalidate(); } else { // 更新鼠标样式 if (radius > 0 && (IsPointNearCircle(e.Location) || IsPointInsideCircle(e.Location))) { pictureBox1.Cursor = Cursors.SizeAll; } else { pictureBox1.Cursor = Cursors.Default; } } } private void PictureBox1_MouseUp(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { isDrawing = false; isMoving = false; pictureBox1.Cursor = Cursors.Default; pictureBox1.Invalidate(); } } private void PictureBox1_Paint(object sender, PaintEventArgs e) { if (sourceBitmap != null && radius > 0) { e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; using (Pen pen = new Pen(Color.Red, 2)) { pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; // 绘制圆形 e.Graphics.DrawEllipse(pen, startPoint.X - radius, startPoint.Y - radius, radius * 2, radius * 2); // 绘制圆心标记 if (isMoving) { e.Graphics.DrawLine(pen, startPoint.X - 5, startPoint.Y, startPoint.X + 5, startPoint.Y); e.Graphics.DrawLine(pen, startPoint.X, startPoint.Y - 5, startPoint.X, startPoint.Y + 5); } } } } private (float scaleX, float scaleY, float offsetX, float offsetY) GetImageScaleAndOffset() { if (sourceBitmap == null || pictureBox1.Image == null) return (1, 1, 0, 0); // 计算图片在 PictureBox 中的实际显示尺寸 float containerRatio = (float)pictureBox1.Width / pictureBox1.Height; float imageRatio = (float)sourceBitmap.Width / sourceBitmap.Height; float scaleX, scaleY, offsetX = 0, offsetY = 0; if (containerRatio > imageRatio) { // 图片高度适应 PictureBox,宽度居中 scaleY = (float)sourceBitmap.Height / pictureBox1.Height; scaleX = scaleY; offsetX = (pictureBox1.Width - (sourceBitmap.Width / scaleX)) / 2; } else { // 图片宽度适应 PictureBox,高度居中 scaleX = (float)sourceBitmap.Width / pictureBox1.Width; scaleY = scaleX; offsetY = (pictureBox1.Height - (sourceBitmap.Height / scaleY)) / 2; } return (scaleX, scaleY, offsetX, offsetY); } private void CaptureButton_Click(object sender, EventArgs e) { if (sourceBitmap != null && radius > 0) { try { using (Mat sourceMat = BitmapConverter.ToMat(sourceBitmap)) using (Mat mask = new Mat(sourceMat.Size(), MatType.CV_8UC1, Scalar.Black)) using (Mat result = new Mat(sourceMat.Size(), sourceMat.Type(), Scalar.Black)) { var (scaleX, scaleY, offsetX, offsetY) = GetImageScaleAndOffset(); // 计算实际图片中的坐标 int actualX = (int)((startPoint.X - offsetX) * scaleX); int actualY = (int)((startPoint.Y - offsetY) * scaleY); int actualRadius = (int)(radius * scaleX); // 使用相同的缩放比例 // 在掩码上绘制白色圆形 Cv2.Circle( mask, new OpenCvSharp.Point(actualX, actualY), actualRadius, Scalar.White, -1); // 应用掩码 sourceMat.CopyTo(result, mask); // 裁剪圆形区域 OpenCvSharp.Rect roi = new OpenCvSharp.Rect( actualX - actualRadius, actualY - actualRadius, actualRadius * 2, actualRadius * 2); // 确保ROI在图像范围内 roi.X = Math.Max(0, roi.X); roi.Y = Math.Max(0, roi.Y); roi.Width = Math.Min(result.Width - roi.X, roi.Width); roi.Height = Math.Min(result.Height - roi.Y, roi.Height); if (roi.Width > 0 && roi.Height > 0) { using (Mat cropped = new Mat(result, roi)) { pictureBox2.Image?.Dispose(); pictureBox2.Image = BitmapConverter.ToBitmap(cropped); } } } } catch (Exception ex) { MessageBox.Show($"截取图片时出错:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } protected override void OnFormClosing(FormClosingEventArgs e) { base.OnFormClosing(e); sourceBitmap?.Dispose(); pictureBox1.Image?.Dispose(); pictureBox2.Image?.Dispose(); } private void btnLoadImage_Click(object sender, EventArgs e) { using (OpenFileDialog openFileDialog = new OpenFileDialog()) { openFileDialog.Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif"; openFileDialog.Title = "选择图片"; if (openFileDialog.ShowDialog() == DialogResult.OK) { try { // 释放之前的图片资源 sourceBitmap?.Dispose(); pictureBox1.Image?.Dispose(); pictureBox2.Image?.Dispose(); // 加载新图片 sourceBitmap = new Bitmap(openFileDialog.FileName); pictureBox1.Image = new Bitmap(sourceBitmap); // 启用截取按钮 btnCapture.Enabled = true; // 重置绘图状态 isDrawing = false; pictureBox1.Invalidate(); } catch (Exception ex) { MessageBox.Show($"加载图片时出错:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } } } }

这个实现提供了一个基础的圆形区域选择和截取功能,可以根据实际需求进行扩展和优化。

本文作者:rick

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!