65.9K
CodeProject 正在变化。 阅读更多。
Home

在 .NET 中进行图像旋转

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (67投票s)

2002年12月7日

BSD

5分钟阅读

viewsIcon

431401

downloadIcon

17392

旋转图像,而无需担心裁剪边缘。

引言

几天前,Tweety 问我如何旋转 Image 对象。  答案似乎很简单,使用 Graphics 对象的 Transform 属性,并带有一个调用了适当的 Rotate 方法的 Matrix 对象。  但我忘记了变换的一个方面,虽然旋转 Image 很容易,但要想让它正确旋转并且仍然大致保持在相同的位置,就需要费一番周折。 

由于看了两周相同的无效代码让我感到厌烦,这次的休假是受欢迎的。 

我的第一次尝试

我想采用最直接的方法,所以我开始尝试使用 Rotate/RotateAt 方法,并试图弄清楚创建适当平移的方程是什么。  不幸的是,我从来没有弄清楚正确的公式。  回顾我之前的图示,我突然想到,我可以计算出旋转后的位图能够容纳的最小可能矩形的尺寸,也就是边界框。 

我的第二次尝试,或者“啊哈!”

给定一个旋转角度 theta,并且知道原始位图的宽度和高度,我可以计算出“空白区域”三角形的大小。 

一些基本的三角恒等式被用来计算三角形边的长度。  假设一个直角三角形,那么

cos(theta) = length(adjacent)/length(hypotenuse)
sin(theta) = length(opposite)/length(hypotenuse)

求解未知量,我们得到

length(adjacent) = cos(theta) * length(hypotenuse)
length(opposite) = sin(theta) * length(hypotenuse)

由于我们已知 thetahypotenuse,我们可以计算出每个三角形另外两条边的长度。  为了说清楚,斜边的长度是原始矩形 r 的宽度或高度。 

现在看图,我们可以看到边界框的宽度将是 oh + aw,高度将是 ah + ow。  我将把证明为什么对于上面的图示中任何矩形 r,最多只有两个不同大小的三角形留给读者作为练习。 

同样,看图可以清楚地知道位图每个角的坐标在旋转时的位置,现在,如果我有一种方法可以在绘制图像时指定每个角的坐标就好了……您知道,我确实有! 

Graphics.DrawImage(Image image, Point[] destPoints);

destPoints 是一个包含 3 个 Point 对象的数组,它定义了一个平行四边形。  你需要传入的三个点定义了原始图像的左上角、右上角和左下角应该绘制的位置。  使用这个方法,你可以轻松地执行缩放和旋转。 

最后要考虑的一点是,上述部分仅在旋转角度介于 0 到 90 度(或 0 到 PI/2 弧度)之间时才有效。  但是处理大于此的角度的旋转很容易,通过使用 cos(theta) 和 sin(theta) 值的绝对值,第一次旋转 90 度将导致返回值从 0 变为 1,下一次旋转则使其从 1 变为 0,如此循环往复,这就是我们想要的行为。  唯一棘手的部分是,每次旋转 90 度时,高度和宽度都需要交换,否则你将基于错误的斜边计算值。 

当位图旋转时,所用的点因 theta 所在的象限而异,因此在计算点时,我必须根据该条件进行分解。 

在 7 行之前的代码片段中,使用了 7 个值,nWidthnHeight 分别是边界框/新位图的宽度和高度。  adjacentTopoppositeTop 是图中标记为“top”的三角形的邻边和对边的长度。  对于 adjacentBottomoppositeBottom 也是如此,只是它使用了另一个三角形;最后一个值当然是 0。  因为三角函数期望所有计算都以弧度进行(而且我反正也更喜欢弧度),所以 theta 已从度数转换为弧度。 

const double pi2 = Math.PI / 2.0;

if( theta >= 0.0 && theta < pi2 )
{
    points = new Point[] { 
        new Point( (int) oppositeBottom, 0 ), 
        new Point( nWidth, (int) oppositeTop ),
        new Point( 0, (int) adjacentBottom )
    };
}
else if( theta >= pi2 && theta < Math.PI )
{
    points = new Point[] { 
        new Point( nWidth, (int) oppositeTop ),
        new Point( (int) adjacentTop, nHeight ),
        new Point( (int) oppositeBottom, 0 )						 
    };
}
else if( theta >= Math.PI && theta < (Math.PI + pi2) )
{
    points = new Point[] { 
        new Point( (int) adjacentTop, nHeight ), 
        new Point( 0, (int) adjacentBottom ),
        new Point( nWidth, (int) oppositeTop )
    };
}
else // theta >= (Math.PI + pi2)
{
    points = new Point[] { 
        new Point( 0, (int) adjacentBottom ), 
        new Point( (int) oppositeBottom, 0 ),
        new Point( (int) adjacentTop, nHeight )		
    };
}

预期用途

我总是创建一个新位图来绘制,而不是使用相同的位图进行旋转。  这有几个原因: 

  1. 行为一致性 - 由于此处的目的是旋转位图而不被裁剪,如果传入的位图不够大,我将不得不创建一个新位图。 
  2. 质量一致性 - 如果同一个位图反复旋转,最终所有的插值操作都会降低图像质量。 

因此,当你传入一个 Image 时,你会得到一个新的 Image,它的质量应该与原始图像相当。 

演示程序非常基础,其中唯一有趣的部分是,我使用了以下代码让 NumericUpDown 控件能够循环。 

if( angle.Value > 359.9m )
{
    angle.Value = 0;
    return ;
}

if( angle.Value < 0.0m )
{
    angle.Value = 359.9m;
    return ;
}

pictureBox.Image = Utilities.RotateImage(img, 
    (float) angle.Value );

设置新值后,我从方法返回,以便由于我所做的更改,它可以再次运行。 

致谢

  • Tweety,感谢你提出的问题,这促使我编写了代码和文章。 
  • Shog9 和 PJ Arends,感谢他们在处理非方形图像代码未运行时提供的建议和指导。 

历史

  • 2002 年 12 月 7 日 - 首次发布
© . All rights reserved.