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

适用于 .NET Compact Framework 的快速图像旋转

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (21投票s)

2007年11月26日

CPOL

7分钟阅读

viewsIcon

123479

downloadIcon

1443

一篇描述如何在 .NET Compact Framework 上实现快速图像旋转的文章

Screenshot - ImageRotationForCF.png

引言

当我为另一篇文章编写应用程序时,我在尝试旋转图像时遇到了一些问题。我正在编写的应用程序是一个 .NET Compact Framework 应用程序,我意识到我在普通 .NET Framework 上使用的 Image.RotateFlip 方法在 Compact Framework 上没有实现。

本文将描述我是如何实现图像旋转方法以及为了获得我想要的性能而必须进行的优化。

Using the Code

文章源代码是一个解决方案,包含一个类库和一个测试应用程序,两者都适用于 .NET Compact Framework,但如果将源文件移动到此类项目中,它们应该可以在普通的 .NET Framework 下编译。

要求

我的代码处理旋转相机/设备倾斜时拍摄的照片(拍摄纵向而非横向照片),因此所需的旋转算法只需要处理 90、180 和 270 度等角度。这与普通 .NET Compact Framework 中的 Image.RotateFlip 方法提供的功能相同(此方法实际上也允许您翻转图像,但我的应用程序不需要翻转,所以我没有实现那部分)。

因此,这个类库的要求非常简单(定义起来简单,但不一定易于实现)

  • 必须能够将图像旋转 90、180 和 270 度。

旋转图像

我能想到的旋转图像的唯一方法是逐像素地将源图像中的像素位置转换为目标图像中的位置。因此,对于一个 2 乘 3 像素的图像,旋转 90 度,转换看起来像这样

Screenshot - PixelTranslation90Degrees.png

以 x, y 坐标表示的映射将是

源 X 源 Y 目标 X 目标 Y
0 0 0 1
1 0 0 0
0 1 1 1
1 1 1 0
0 2 2 1
1 2 2 0

一个模式出现了!

在代码中,此模式可以表示为

int destinationX = rotatedImageWidth - 1 - sourceY;
int destinationY = sourceX;    

180 度和 270 度的模式略有不同,但遵循相同的原理。

获取像素

现在我们知道源图像中的每个像素在旋转后会移动到目标图像的何处,我们现在需要做的就是遍历所有像素,从源中获取 Color 并将其应用到目标。那么我们如何做到这一点呢?实际上非常容易,因为 System.Drawing.Bitmap 有以下方法:

for (int y = 0; y < sourceImage.Height; ++y)
{
    for (int x = 0; x < sourceImage.Width; ++x)
    {
        destinationImage.SetPixel(
            sourceImage.Width - 1 - y, 
            x, 
            originalBitmap.GetPixel(x, y));
    }
}

基本上就是这样,这将使我们的图像旋转 90 度。
所以我实现了我的旋转函数来做这件事,并在一个 800 乘 600 像素的图像上测试了它,结果什么也没发生!我仔细检查了代码,但找不到任何问题,所以我做了我通常做的事情;我又试了一次。还是没有。所以我去喝了杯咖啡。我的实现包含计时旋转的代码,我从一开始就这么做了,因为我预计它会相当慢,当我拿着咖啡回来时,图像已经旋转了,耗时 2 分 53 秒。一点也不快,比我预期的慢得多。

我决定需要优化我的方法。

第一级优化

我开始优化简单的部分,并希望(但实际上并不相信)这能足够加快我的算法,使其以不错的速度运行。
我尽可能地预先计算。因为我每个像素都需要 sourceImage.Width - 1 的值,但该值在整个旋转过程中是相同的,所以我将那部分移到循环外面。

// Pre-calculated
int sourceImageWidthMinusOne = sourceImage.Width - 1;
for (int y = 0; y < sourceImage.Height; ++y)
{
    for (int x = 0; x < sourceImage.Width; ++x)
    {
        destinationImage.SetPixel(
            sourceImageWidthMinusOne - y, 
            x, 
            originalBitmap.GetPixel(x, y));
    }
}

我还尝试通过将 .Width.Height 调用的结果存储在循环外部来删除任何方法调用。这应该会加快速度,因为现在调用的方法少了 sourceImage.Width 乘以 sourceImage.Height 次(请记住,属性也是方法)。

// Pre-stored
int sourceImageWidth = sourceImage.Widht;
int sourceImageHeight = sourceImage.Height;
// Pre-calculated
int sourceImageWidthMinusOne = sourceImageWidth - 1;

for (int y = 0; y < sourceImageHeight; ++y)
{
    for (int x = 0; x < sourceImageWidth; ++x)
    {
        destinationImage.SetPixel(
            sourceImageWidthMinusOne - y, 
            x, 
            originalBitmap.GetPixel(x, y));
    }
}

经过这些惊人的优化后,我重新运行了我的测试应用程序。
结果:图像在 2 分 45 秒内旋转。几乎不值得付出努力。

既然我轻量级的优化攻击失败了,是时候拿出大家伙了。而所谓大家伙,我指的是指针。还有 unsafe 代码。

第二级优化

由于使用 Bitmap 提供的方法获取和设置像素太慢,我需要其他方式来获取这些数据。可以通过从 Bitmap 获取 BitmapData 对象来以相当底层的方式访问像素数据。有一个方法 Bitmap.LockBits 可以将表示像素的位“锁定”到系统内存中,并返回一个 BitmapData 对象。

// original here means source
// rotated here means destination
BitmapData originalData = originalBitmap.LockBits(
    new Rectangle(0, 0, originalWidth, originalHeight), 
    ImageLockMode.ReadOnly, 
    PixelFormat.Format32bppRgb);

BitmapData rotatedData = rotatedBitmap.LockBits(
    new Rectangle(0, 0, rotatedBitmap.Width, rotatedBitmap.Height), 
    ImageLockMode.WriteOnly, 
    PixelFormat.Format32bppRgb);

在锁定位时,您必须指定一个 ImageLockMode,我为源使用只读锁(因为我只读取像素),为目标使用只写锁(因为我需要写入但不需要读取这些像素)。
此外,您必须指定一个 PixelFormat,我将其硬编码为 PixelFormat.Format32bppRgb,这意味着我的代码只适用于每像素 32 位的图像。如果您在使用此代码时遇到问题,请注意这一点。

一旦位被锁定,可以通过调用 BitmapData.ScanO 来获取指向第一个像素数据的指针,该方法返回一个 IntPtr,但我打算使用指针算术来加速处理,因此仅仅拥有一个 IntPtr 是不够的,我需要一个 int*。通过拥有一个“合适”的指针,我可以(或多或少地)做任何我想做的事情,而且速度惊人!

“合适的”指针是这样获得的

unsafe
{
    int* originalPointer = (int*)originalData.Scan0.ToPointer();
    int* rotatedPointer = (int*)rotatedData.Scan0.ToPointer();
}

请注意 unsafe 关键字的使用,因为拥有“正确”的指针将允许我对内存做任何事情,这被认为是“不安全”的,因此您必须明确告诉编译器这是您的意图(您还需要在项目属性中允许不安全代码)。

有了我的正确指针,我可以轻松操作存储像素数据的内存,我的图像旋转代码现在看起来像这样

for (int y = 0; y < originalHeight; ++y)
{
    int destinationX = newWidthMinusOne - y;
    for (int x = 0; x < originalWidth; ++x)
    {
        int sourcePosition = (x + y * originalWidth);
        int destinationY = x;
        int destinationPosition = (destinationX + destinationY * newWidth);

        // Here I read and assign the pixel value
        rotatedPointer[destinationPosition] = originalPointer[sourcePosition];
    }
}

释放对位获得的锁很重要,这在所有旋转完成后进行。

// We have to remember to unlock the bits when we're done.
originalBitmap.UnlockBits(originalData);
rotatedBitmap.UnlockBits(rotatedData);

那些是我当时能想到的所有优化,完整的旋转函数现在看起来像这样:

private static void InternalRotateImage(int rotationAngle, 
                                        Bitmap originalBitmap, 
                                        Bitmap rotatedBitmap)
{
    // It should be faster to access values stored on the stack
    // compared to calling a method (in this case a property) to 
    // retrieve a value. That's why we store the width and height
    // of the bitmaps here so that when we're traversing the pixels
    // we won't have to call more methods than necessary.

    int newWidth = rotatedBitmap.Width;
    int newHeight = rotatedBitmap.Height;

    int originalWidth = originalBitmap.Width;
    int originalHeight = originalBitmap.Height;

    // We're going to use the new width and height minus one a lot so lets 
    // pre-calculate that once to save some more time
    int newWidthMinusOne = newWidth - 1;
    int newHeightMinusOne = newHeight - 1;

    // To grab the raw bitmap data into a BitmapData object we need to
    // "lock" the data (bits that make up the image) into system memory.
    // We lock the source image as ReadOnly and the destination image
    // as WriteOnly and hope that the .NET Framework can perform some
    // sort of optimization based on this.
    // Note that this piece of code relies on the PixelFormat of the 
    // images to be 32 bpp (bits per pixel). We're not interested in 
    // the order of the components (red, green, blue and alpha) as 
    // we're going to copy the entire 32 bits as they are.
    BitmapData originalData = originalBitmap.LockBits(
        new Rectangle(0, 0, originalWidth, originalHeight), 
        ImageLockMode.ReadOnly, 
        PixelFormat.Format32bppRgb);
    BitmapData rotatedData = rotatedBitmap.LockBits(
        new Rectangle(0, 0, rotatedBitmap.Width, rotatedBitmap.Height), 
        ImageLockMode.WriteOnly, 
        PixelFormat.Format32bppRgb);

    // We're not allowed to use pointers in "safe" code so this
    // section has to be marked as "unsafe". Cool!
    unsafe
    {
        // Grab int pointers to the source image data and the 
        // destination image data. We can think of this pointer
        // as a reference to the first pixel on the first row of the 
        // image. It's actually a pointer to the piece of memory 
        // holding the int pixel data and we're going to treat it as
        // an array of one dimension later on to address the pixels.
        int* originalPointer = (int*)originalData.Scan0.ToPointer();
        int* rotatedPointer = (int*)rotatedData.Scan0.ToPointer();

        // There are nested for-loops in all of these case statements
        // and one might argue that it would have been neater and more
        // tidy to have the switch statement inside the a single nested
        // set of for loops, doing it this way saves us up to three int 
        // to int comparisons per pixel. 
        //
        switch (rotationAngle)
        {
            case 90:
                for (int y = 0; y < originalHeight; ++y)
                {
                    int destinationX = newWidthMinusOne - y;
                    for (int x = 0; x < originalWidth; ++x)
                    {
                        int sourcePosition = (x + y * originalWidth);
                        int destinationY = x;
                        int destinationPosition = 
                                (destinationX + destinationY * newWidth);
                        rotatedPointer[destinationPosition] = 
                            originalPointer[sourcePosition];
                    }
                }
                break;
            case 180:
                for (int y = 0; y < originalHeight; ++y)
                {
                    int destinationY = (newHeightMinusOne - y) * newWidth;
                    for (int x = 0; x < originalWidth; ++x)
                    {
                        int sourcePosition = (x + y * originalWidth);
                        int destinationX = newWidthMinusOne - x;
                        int destinationPosition = (destinationX + destinationY);
                        rotatedPointer[destinationPosition] = 
                            originalPointer[sourcePosition];
                    }
                }
                break;
            case 270:
                for (int y = 0; y < originalHeight; ++y)
                {
                    int destinationX = y;
                    for (int x = 0; x < originalWidth; ++x)
                    {
                        int sourcePosition = (x + y * originalWidth);
                        int destinationY = newHeightMinusOne - x;
                        int destinationPosition = 
                            (destinationX + destinationY * newWidth);
                        rotatedPointer[destinationPosition] = 
                            originalPointer[sourcePosition];
                    }
                }
                break;
        }

        // We have to remember to unlock the bits when we're done.
        originalBitmap.UnlockBits(originalData);
        rotatedBitmap.UnlockBits(rotatedData);
    }
}

该方法被声明为

private static void InternalRotateImage(...)    

而不是可能更合乎逻辑和有用的

 public static Image RotateImage(...)    

是因为我的类库包含两种实现,一种是快速不安全的版本,另一种是慢速安全的版本,使用哪一种在编译时通过预处理器指令决定。您可能会想知道我为什么使用预处理器指令来决定使用哪种实现,而不是例如布尔参数(bool useFastVersion)。如果您的程序集要标记为安全,则其中不能存在任何不安全代码,无论是否调用它。这就是为什么需要预处理器指令。

最终结果

那么,实现结果如何呢?

首先,看看我必须实现的一个要求,这个类库能够将图像旋转 90、180 和 270 度。这意味着我所有的要求都得到了满足。太棒了!

但是由于本文大部分内容都是关于优化的,您可能会想知道最终实现的运行速度有多快,我很高兴地说它实际上相当快。旋转图像不再需要几分钟,现在一张 800 乘 600 像素的图像在不到 5 秒内就能旋转完成。而且通常不到 3 秒。我认为这是相当可以接受的。

测试应用程序允许您旋转图像资源并测量所需时间,对于测试图像(410 x 312 像素),旋转时间不到一秒。

Screenshot - ImageRotationFull.png

关注点

我认为这个实现最有趣的部分是,它可以在不调用 GDI 库中的本机代码的情况下获得不错的速度。我并不反对使用本机 DLL,但我发现如果能避免它,那会更简洁。

欢迎所有评论和建议。

历史

  • 2007-11-26:第一版
© . All rights reserved.