You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C# WinForm中实现手绘图形自动转为规整图形?

实现手绘形状规整功能(转直线/完美正方形)

核心实现思路

  • 记录用户在Panel A上的所有手绘轨迹点,而非直接绘制(避免重绘丢失,同时方便后续形状分析)
  • 用户结束绘制(MouseUp事件)时,对轨迹点做分析:
    • 若轨迹接近直线,计算拟合后的标准直线并绘制到Panel B
    • 若轨迹接近正方形,生成边长一致、角度为90度的完美正方形并绘制到Panel B
  • 用Bitmap存储Panel A的手绘内容,解决窗口重绘导致的内容丢失问题

改造后的完整代码

public partial class frmPaint : Form
{
    // 存储手绘轨迹的所有点
    private List<Point> _drawPoints = new List<Point>();
    // 保存Panel A的手绘内容,用于重绘
    private Bitmap _drawBitmap;
    private Pen _penA = new Pen(Color.Red, 2);
    private Pen _eraser = new Pen(Color.White, 10);

    public frmPaint()
    {
        InitializeComponent();
        // 初始化Bitmap,尺寸与Panel A一致
        _drawBitmap = new Bitmap(panelDraw.Width, panelDraw.Height);
        // 初始化Panel B的白色背景
        using (var g = panelResult.CreateGraphics())
        {
            g.Clear(Color.White);
        }
    }

    private void panelDraw_MouseDown(object sender, MouseEventArgs e)
    {
        // 开始绘制时清空上一次的轨迹点
        _drawPoints.Clear();
        _drawPoints.Add(e.Location);

        // 更新画笔宽度
        if (rbLineWidth5.Checked)
            _penA.Width = 5;
        if (rbLineWidth10.Checked)
            _penA.Width = 10;
        if (rbLineWidth15.Checked)
            _penA.Width = 15;
    }

    private void panelDraw_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            _drawPoints.Add(e.Location);
            // 在Bitmap上绘制当前轨迹段
            using (var g = Graphics.FromImage(_drawBitmap))
            {
                g.DrawLine(_penA, _drawPoints[^2], _drawPoints[^1]);
            }
            // 刷新Panel A显示最新内容
            panelDraw.Invalidate();
        }
        else if (e.Button == MouseButtons.Right)
        {
            _drawPoints.Add(e.Location);
            // 橡皮擦功能:用白色线条覆盖原有内容
            using (var g = Graphics.FromImage(_drawBitmap))
            {
                g.DrawLine(_eraser, _drawPoints[^2], _drawPoints[^1]);
            }
            panelDraw.Invalidate();
        }
    }

    private void panelDraw_MouseUp(object sender, MouseEventArgs e)
    {
        // 绘制结束后,分析轨迹并在Panel B生成规整图形
        AnalyzeAndDrawShape();
    }

    private void panelDraw_Paint(object sender, PaintEventArgs e)
    {
        // 重绘时展示Bitmap中的内容,避免内容丢失
        e.Graphics.DrawImage(_drawBitmap, 0, 0);
    }

    private void btnColor_Click(object sender, EventArgs e)
    {
        using (ColorDialog colorDialog = new ColorDialog())
        {
            if (colorDialog.ShowDialog() == DialogResult.OK)
                _penA.Color = colorDialog.Color;
        }
    }

    // 分析轨迹形状,绘制规整后的图形到Panel B
    private void AnalyzeAndDrawShape()
    {
        if (_drawPoints.Count < 2) return;

        // 清空Panel B的原有内容
        using (var g = panelResult.CreateGraphics())
        {
            g.Clear(Color.White);
        }

        // 判断是否为近似直线
        if (IsApproximateLine(_drawPoints, 10))
        {
            var (start, end) = FitLine(_drawPoints);
            using (var g = panelResult.CreateGraphics())
            {
                g.DrawLine(_penA, start, end);
            }
        }
        // 判断是否为近似正方形
        else if (IsApproximateSquare(_drawPoints, 0.15))
        {
            var squarePoints = GetPerfectSquare(_drawPoints);
            using (var g = panelResult.CreateGraphics())
            {
                g.DrawPolygon(_penA, squarePoints);
            }
        }
    }

    // 判断轨迹是否接近直线,maxAvgDistance为允许的平均像素误差
    private bool IsApproximateLine(List<Point> points, double maxAvgDistance)
    {
        if (points.Count < 2) return false;
        var (start, end) = FitLine(points);
        double totalDistance = 0;
        foreach (var p in points)
        {
            totalDistance += GetDistanceToLine(p, start, end);
        }
        double avgDistance = totalDistance / points.Count;
        return avgDistance <= maxAvgDistance;
    }

    // 用最小二乘法拟合直线,返回拟合后的起止点
    private (Point Start, Point End) FitLine(List<Point> points)
    {
        int n = points.Count;
        double sumX = points.Sum(p => p.X);
        double sumY = points.Sum(p => p.Y);
        double sumXY = points.Sum(p => p.X * p.Y);
        double sumX2 = points.Sum(p => p.X * p.X);

        double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
        double intercept = (sumY - slope * sumX) / n;

        // 取轨迹的最小/最大X值作为直线端点
        int minX = points.Min(p => p.X);
        int maxX = points.Max(p => p.X);
        Point start = new Point(minX, (int)(slope * minX + intercept));
        Point end = new Point(maxX, (int)(slope * maxX + intercept));
        return (start, end);
    }

    // 计算点到直线的垂直距离
    private double GetDistanceToLine(Point p, Point lineStart, Point lineEnd)
    {
        double numerator = Math.Abs((lineEnd.Y - lineStart.Y) * p.X - (lineEnd.X - lineStart.X) * p.Y + lineEnd.X * lineStart.Y - lineEnd.Y * lineStart.X);
        double denominator = Math.Sqrt(Math.Pow(lineEnd.Y - lineStart.Y, 2) + Math.Pow(lineEnd.X - lineStart.X, 2));
        return denominator == 0 ? 0 : numerator / denominator;
    }

    // 判断轨迹是否接近正方形,errorThreshold为边长比例的允许误差
    private bool IsApproximateSquare(List<Point> points, double errorThreshold)
    {
        if (points.Count < 4) return false;
        // 获取轨迹的边界矩形
        var rect = GetBoundingRectangle(points);
        double width = rect.Width;
        double height = rect.Height;
        // 宽高比接近1
        double ratio = Math.Min(width, height) / Math.Max(width, height);
        if (ratio < 1 - errorThreshold) return false;

        // 验证轨迹点是否覆盖矩形的四个边区域
        bool hasLeft = points.Any(p => Math.Abs(p.X - rect.Left) < 10);
        bool hasRight = points.Any(p => Math.Abs(p.X - rect.Right) < 10);
        bool hasTop = points.Any(p => Math.Abs(p.Y - rect.Top) < 10);
        bool hasBottom = points.Any(p => Math.Abs(p.Y - rect.Bottom) < 10);
        return hasLeft && hasRight && hasTop && hasBottom;
    }

    // 获取轨迹点的边界矩形
    private Rectangle GetBoundingRectangle(List<Point> points)
    {
        int minX = points.Min(p => p.X);
        int maxX = points.Max(p => p.X);
        int minY = points.Min(p => p.Y);
        int maxY = points.Max(p => p.Y);
        return new Rectangle(minX, minY, maxX - minX, maxY - minY);
    }

    // 根据轨迹边界生成完美正方形
    private Point[] GetPerfectSquare(List<Point> points)
    {
        var rect = GetBoundingRectangle(points);
        // 取边界矩形的最大边长作为正方形边长
        int sideLength = Math.Max(rect.Width, rect.Height);
        int centerX = rect.Left + rect.Width / 2;
        int centerY = rect.Top + rect.Height / 2;
        // 生成以原矩形中心为中心的正方形
        int left = centerX - sideLength / 2;
        int top = centerY - sideLength / 2;
        return new Point[]
        {
            new Point(left, top),
            new Point(left + sideLength, top),
            new Point(left + sideLength, top + sideLength),
            new Point(left, top + sideLength),
            new Point(left, top) // 闭合图形
        };
    }
}

关键逻辑说明

  1. 轨迹持久化:用List<Point>记录所有手绘点,配合Bitmap存储绘制内容,通过Paint事件实现重绘,解决原生CreateGraphics导致的内容丢失问题。
  2. 直线识别:通过最小二乘法拟合轨迹直线,计算所有点到直线的平均距离,判断是否符合直线特征。
  3. 正方形识别与规整:先获取轨迹的边界矩形,验证宽高比是否接近1,再确认轨迹点覆盖矩形四边;最后生成以原矩形中心为基准的完美正方形。
  4. 交互触发:在用户松开鼠标时触发形状分析,将规整后的图形实时绘制到Panel B。

内容的提问来源于stack exchange,提问作者Mardibak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 22:25:05