过眼•物影天狼

博客通天下 淡墨书豪辞

Flower

Archive for the ‘图形图像’ Category

Hermite 曲线的算法与实现

      摘要:本文主要介绍了一种自由曲线 Hermite 曲线的数学原理,并以此为依据构造了 Hermite 曲线的计算机算法,并在微软的 .net 框架下使用 GDI+ 实现。

      关键字:合成曲线,Hermite曲线,C#,GDI+

一、数学原理
      曲线是构建几何模型的最基本元素之一,主要分为解析曲线和合成曲线。解析曲线通常是先有曲线方程,然后才能把曲线画出来,这种方式对于曲线的构造者来说是非常复杂且不直观的,而且改变曲线的一些参数,设计者也无法立刻了解曲线形状会做怎样的变化。在作曲线设计时,设计者通常希望能先将大致形状用很直观的方式描绘出来,并能够很容易的依照所需要的形状作修改,因此合成曲线是比较合适的方式。

      合成曲线通常以参数的形式来表现,是由设计者根据其设计的需求和几何信息,去“合成”出这条曲线。这样由设计者输入的几何数据,就是曲线的控制点。

      一般的合成曲线,至少需要一个三次的参数式:

公式(1)

用向量表示为:

公式(2)

如何确定式中的参数,就形成了不同的合成曲线的构造方法。Hermite 曲线就是通过曲线的起点(P0)、终点(P1)、起点切向量(V0)和终点切向量(V1)来确定曲线的。改变这四个参数,就可以控制 Hermite 曲线的形状。图1就是构建 Hermite 曲线的示意图。

图1

当给定以上四个参数之后,如何来确定这条 Hermite 曲线呢?

首先将(2)式作一次微分得到:

公式(3)

然后将 u=0,u=1 代入(2)式和(3)式中,就可以得到参数式中的系数:

公式(4)

将(4)式作进一步整理,最后可以将 Hermite 曲线方程写成如下形式:

公式(5)

这就是 Hermite 曲线的参数方程。根据此方程,对于 P0=(-2,2),P1=(3,-1),V0=(8,10) 和 V1=(15,10) 这四个参数,可以构造如下的表格:

使用 Matlab 绘制此曲线,得到图2:

图2

      但是在做曲线设计时,Hermite 曲线仍然存在不少问题,例如在建立 Hermite 曲线时设计者必须输入曲线两端切向量的大小和方向,这对设计者来讲仍然是不直观的。另外,Hermite 曲线不具有区域控制的能力,在建立 Hermite 曲线时提供的是个参数,改变任何一项输入,整条曲线的形状都会发生变化,设计者很难对其进行局部的、小范围的修改。

二、算法
      有了以上的数学基础,构造算法就不是件困难的事情。

/// <summary>
/// 绘制Hermite曲线的核心方法
/// </summary>
/// <param name="pen">绘制曲线用的Pen对象</param>
/// <param name="p0">起点坐标</param>
/// <param name="p1">终点坐标</param>
/// <param name="v0">起点切向量</param>
/// <param name="v1">终点切向量</param>
private void DrawHermite(Pen pen,Point p0, Point p1, Point v0, Point v1) {
    // 计算出来的当前坐标
    int x = p0.X;
    int y = p0.Y;

    // 计算出来的前一个坐标,使用该两个做标连成一条线,来绘制曲线
    int preX, preY;

    // 根据参数计算每个点的坐标,参数的增量为0.01
    for (double i = 0.0; i <= 1.0; i = i + 0.01) {
        preX = x;
        preY = y;

        // 保存计算中间结果,避免重复计算,提高算法效率
        double i2 = i * i;
        double i3 = i2 * i;
        double express = 3 * i2 - 2 * i3;

        // 计算横坐标和纵坐标
        x = (int)((1 - express) * p0.X + express * p1.X + (i - 2 * i2 + i3) * v0.X + (i3 - i2) * v1.X);
        y = (int)((1 - express) * p0.Y + express * p1.Y + (i - 2 * i2 + i3) * v0.Y + (i3 - i2) * v1.Y);

        // 画线
        this.drawingSurface.DrawLine(pen, preX, preY, x, y);
    }

    this.drawingSurface.DrawLine(pen, x, y, p1.X, p1.Y);
}

三、使用 GDI+ 实现
      在 .net 框架下,使用 GDI+ 实现这个算法是件轻松的事情。但是在编程过程中仍然出现了几个问题。

      首先,如何确定两个起点和两个切向量。本程序采用了如下的方法:先选定一个点,然后拉出一条直线,以该点为起点(或终点)并以该直线的方向和长度作为起点(或终点)切向量的方向和大小。

      其次,本程序可以实现类似 PhotoShop 中的钢笔功能。所以有一个如何产生拉动的效果的问题。这对这个问题使用了两种不同的解决方法:1、对于画曲线,使用了两个画笔,一个用于绘制,一个用于擦除。当鼠标移动的时候,就会使用绘制的画笔绘制新曲线,并用擦除画笔擦除刚才的曲线。但该方法会导致另一个问题,就是当该曲线覆盖到其他线条上之后,当曲线离开后,该线条就会有部分被擦掉。但是想解决这个问题是很困难的。(不知道 PhotoShop 是如何实现的。)2、画直线的时候,使用了 GDI+ 中自带的一个 ControlPaint.DrawReversibleLine() 方法,该方法可以自己解决以上的问题。

      第三、像这样一遍一遍的重画和擦除,会很占用系统资源,但是没有什么更好的解决方法,从网上找到的文章来看,如果复写(Override)OnMouseDown, OnMouseMove 和 OnMouseUp 事件,会比处理此三个事件的方法要来得效率高一些,因此本程序的所有事件全部采用了这种方法。下面的这段代码,是复写了 OnMouseMove 事件,用于处理当鼠标按下左键移动的时候,产生的拉动效果。

protected override void OnMouseMove(MouseEventArgs e) {
    // 判断当鼠标移动的时候是否有鼠标左键按下
    if (e.Button == MouseButtons.Left) {
        // isContinuedDrawing 是个标志变量,标志所产时的动作是否为了画第二个参量
        if (!isContinedDrawing) {
            endPoint[0].X = e.X;
            endPoint[0].Y = e.Y;

            // 使用 ControlPaint 画直线
            ControlPaint.DrawReversibleLine(PointToScreen(startPoint[0])PointToScreen(previousPoint), Color.Black);
            ControlPaint.DrawReversibleLine(PointToScreen(startPoint[0]), PointToScreen(endPoint[0]), Color.Black);

            previousPoint = endPoint[0];
        } else {
            endPoint[1].X = e.X;
            endPoint[1].Y = e.Y; ControlPaint.DrawReversibleLine(PointToScreen(startPoint[1]), PointToScreen(previousPoint), Color.Black);
            
            Point _v0 = new Point();
            _v0.X = endPoint[0].X - startPoint[0].X;
            _v0.Y = endPoint[0].Y - startPoint[0].Y;

            Point _v1 = new Point();
            _v1.X = previousPoint.X - startPoint[1].X;
            _v1.Y = previousPoint.Y - startPoint[1].Y;

            // 擦除以前的曲线
            this.DrawHermite(erasePen,startPoint[0], startPoint[1], _v0, _v1);
            // 画新的曲线
            ControlPaint.DrawReversibleLine(PointToScreen(startPoint[1]), PointToScreen(endPoint[1]), Color.Black);
            _v1.X = endPoint[1].X - startPoint[1].X;
            _v1.Y = endPoint[1].Y - startPoint[1].Y;

            this.DrawHermite(drawPen,startPoint[0], startPoint[1], _v0, _v1);
            previousPoint = endPoint[1];
        }
    }

    // 调用父类中的 OnMouseOver 事件三
    base.OnMouseMove(e);
}

四、程序截图

五、参考文献

使用 C# 实现图像的边缘检测

      我本人对图像处理没什么兴趣,要不是这门课要交作业,我才懒得做这些东西。唉……不过程序写了,自然会有一点想法,发到 Blog 上,以备后用吧。但是,即便是写了程序,也仍然不知道作边缘检测的原理何在,只是模糊的知道大概是对图像灰度求梯度,梯度大的就是边缘了。但毕竟图像是离散化的,可以使用另外的方法求梯度,而不用像高数中那样拼命地算偏导数了。有很多学者提出了很多种不同性能的模板,只要按照模板作简单的四则运算就行了,当然这也是能用程序实现的关键。由于作为教学课程,所有的内容都是以简单的灰度图像来说明举例的,当然边缘检测也不例外,留作业写程序也是一样,所以马上就遇到的一个问题是如何将彩色图像转为灰度图像。在课程中,都是简单的认为灰度图像只有一个亮度,这自然是没错的,但是放到计算机里,灰度也是一种颜色,是颜色就要使用色彩模式(最常用的自然是 RGB 了),那么这种灰度到底应该是怎样的颜色编码呢?索性取向 Photoshop ,看一看各种灰度色调,终于有所发祥。其实也可以这么想,全黑是 #000000 ,全白是 #FFFFFF ,那么是不是只要 RGB 值都相等,这个颜色就是灰度色呢?试了一下,果然如此。这样就好办了,至少第一步知道了转换的目标是什么了。但马上就又有了一个问题,彩色图片的颜色这么多,那么如何知道哪种彩色颜色对应哪种灰度颜色呢?这一点我从 .net Framework 中找到了答案。其实我从一开始就想在 .net Framework 中寻找有没有直接将 RGB 转成灰度或者是 HIS 模式(因为 HIS 模式中的 I 就使亮度,自然就容易转成灰度了)的,是不是太奢望了,所以我也没抱太大的希望,但是在这过程中却发现 Color 中有关一个实例方法 GetBrightness() ,就是用来获得颜色亮度的,真是踏破铁鞋无觅处,得来全不费功夫。该方法返回一个 0~1 之间的浮点数,那么如果 RGB 每个各占一个字节的话,那么刚好可以用这个值去乘以 255 ,然后拼成一个 RGB ,这个颜色就是原始色彩所对应的灰色。程序代码如下:

//定义两个颜色变量,oColor为原始色彩,gColor为对应的灰度色彩
Color oColor,gColor;
//原始色彩的亮度
float brightness;
//灰度色彩用 RGB 来表示,由于 R=G=B 所以只用一个变量就可以了
int gRGB;
//遍历图像中的每个像素
for (int i = 0; i < oBmp.Width; i ++) {
    for (int j = 0; j < oBmp.Height; j ++) {
        //得到像素的原始色彩        
        oColor = oBmp.GetPixel(i,j);
        //得到该色彩的亮度
        brightness = oColor.GetBrightness();
        //用该亮度计算灰度
        gRGB = (int)(brightness * 255);
        //组成灰度色彩
        gColor = Color.FromArgb(gRGB,gRGB,gRGB);
        //最后将该灰度色彩赋予该像素
        gBmp.SetPixel(i,j,gColor); 
    }
}

其实还是很简单的。这之后就可以按照书中所说的模板游历的方法来进行边缘检测了。程序如下:
//template为模板,nThreshold是一个阈值,
//用来将模板游历的结果(也就是梯度)进行划分。
//大于阈值的和小于阈值的分别赋予两种颜色,白或黑来标志边界和背景
private void EdgeDectect(int[,] template,int nThreshold) {
    //取出和模板等大的原图中的区域
    int[,] gRGB = new int[3,3];
    //模板值结果,梯度
    int templateValue = 0;
    //遍历灰度图中每个像素
    for (int i = 1; i < gBmp.Width - 1; i ++) {
        for (int j = 1; j < gBmp.Height - 1; j ++) {
            //取得模板下区域的颜色,即灰度
            gRGB[0,0] = gBmp.GetPixel(i-1,j-1).R;
            gRGB[0,1] = gBmp.GetPixel(i-1,j).R;
            gRGB[0,2] = gBmp.GetPixel(i-1,j+1).R;
            gRGB[1,0] = gBmp.GetPixel(i,j-1).R;
            gRGB[1,1] = gBmp.GetPixel(i,j).R;
            gRGB[1,2] = gBmp.GetPixel(i,j+1).R;
            gRGB[2,0] = gBmp.GetPixel(i+1,j-1).R;
            gRGB[2,1] = gBmp.GetPixel(i+1,j).R;
            gRGB[2,2] = gBmp.GetPixel(i+1,j+1).R;
            //按模板计算
            for (int m = 0; m < 3; m ++) {
                for (int n = 0; n < 3; n ++) {
                    templateValue += template[m,n] * gRGB[m,n];
                }
            }
            //将梯度之按阈值分类,并赋予不同的颜色
            if (templateValue > nThreshold) {
                eBmp.SetPixel(i,j,Color.FromArgb(255,255,255)); //白
            } else {
                eBmp.SetPixel(i,j,Color.FromArgb(0,0,0)); //黑
            }
            templateValue = 0;
        }
    }
}

You are currently browsing the archives for the 图形图像 category.