分析图像的颜色

Analyze colors of an Image

我裁剪了图像的一部分,并通过 12 个轨迹栏定义了 2 个颜色范围 (H/S/L)。我还有一个 "Precision/Speed" 滑块,范围从 1 到 10。

我需要分析图像中有多少像素落入每个指定的颜色范围。
基于 precision/speed 滑块,我跳过了一些 rows/pixels。

它运行良好,但速度太慢。高精度(trackbar value = 1),大约需要550毫秒。
精度低但速度快(trackbar 值 = 10)大约需要 5 毫秒。

有没有办法加快这段代码的速度?理想情况下,我需要它快 5 倍。

 For y As Integer = 0 To 395
    If y Mod 2 = 0 Then
        startpixel = tbval / 2
    Else
        startpixel = 0
    End If

    If y Mod tbval = 0 Then
        For x As Integer = 0 To 1370
            If x Mod tbval - startpixel = 0 Then
                analyzedpixels = analyzedpixels + 1

                Dim pColor As Color = crop.GetPixel(x, y)
                Dim h As Integer = pColor.GetHue
                Dim s As Integer = pColor.GetSaturation * 100
                Dim l As Integer = pColor.GetBrightness * 100

                'verify if it is part of the first color

                If h >= h1min And h <= h1max And s >= s1min And s <= s1max And l >= l1min And l <= l1max Then
                    color1pixels = color1pixels + 1
                End If

                If h >= h2min And h <= h2max And s >= s2min And s <= s2max And l >= l2min And l <= l2max Then
                    color2pixels = color2pixels + 1
                End If
            End If
        Next
    End If
Next

编辑:

这是工作代码..

Dim rect As New Rectangle(0, 0, crop.Width, crop.Height)
Dim bdata As Imaging.BitmapData = crop.LockBits(rect, Imaging.ImageLockMode.ReadOnly, crop.PixelFormat)

Dim ptr As IntPtr = bdata.Scan0
Dim bytes As Integer = Math.Abs(bdata.Stride) * crop.Height
Dim rgbValues As Byte() = New Byte(bytes - 1) {}
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes)

For i As Integer = 0 To crop.Height - 1

    If i Mod 2 = 0 Then
        startpixel = tbval / 2
    Else
        startpixel = 0
    End If

    If i Mod tbval = 0 Then
        For j As Integer = 0 To crop.Width - 1
            If j Mod tbval - startpixel = 0 Then

                analyzedpixels = analyzedpixels + 1
                Dim position = (bdata.Stride * i) + j * 4
                Dim c = Color.FromArgb(BitConverter.ToInt32(rgbValues, position))
                Dim h As Integer = c.GetHue
                Dim s As Integer = c.GetSaturation * 100
                Dim l As Integer = c.GetBrightness * 100

                If h >= h1min And h <= h1max And s >= s1min And s <= s1max And l >= l1min And l <= l1max Then
                    color1pixels = color1pixels + 1
                End If

                If h >= h2min And h <= h2max And s >= s2min And s <= s2max And l >= l2min And l <= l2max Then
                    color2pixels = color2pixels + 1
                End If
            End If
            stride += 4
        Next
    End If
Next

crop.UnlockBits(bdata)

当对 Bitmap 的颜色数据执行顺序操作时,Bitmap.LockBits 方法可以显着提高性能,因为 Bitmap 数据只需要在内存中加载一次,而不是顺序操作 GetPixel/SetPixel调用:每次调用都会加载内存中的一部分Bitmap数据然后丢弃,再次调用这些方法时重复这个过程。

如果需要对 GetPixel/SetPixel 的单个调用,则这些方法可能比 Bitmap.LockBits() 具有性能优势。但是,在这种情况下,性能实际上并不是一个因素。

Bitmap.LockBits() 的工作原理

这是函数调用:

public BitmapData LockBits (Rectangle rect, ImageLockMode flags, PixelFormat format);
// VB.Net
Public LockBits (rect As Rectangle, flags As ImageLockMode, format As PixelFormat) As BitmapData
  • rect As Rectangle:该参数指定了我们感兴趣的Bitmap数据的部分;此部分的字节将加载到内存中。它可以是位图的整个大小,也可以是它的一小部分。

  • flags As ImageLockMode:指定要执行的锁定类型。对内存的访问可以限制为读取或写入,或者允许并发 Read/Write 操作。
    它还可以用于指定 - 设置 ImageLockMode.UserInputBuffer - BitmapData 对象由调用代码提供。
    BitmapData对象定义了Bitmap的一些属性(Bitmap的WidthHeight,扫描线的宽度(Stride:组成单行像素的字节数,由Bitmap.Width乘以每个像素的字节数表示,四舍五入为4字节边界。见注意 Stride).
    BitmapData.Scan0 属性 是指向存储位图数据的初始内存位置的指针 (IntPtr)。
    此 属性 允许指定已存储预先存在的位图数据缓冲区的内存位置。当使用指针在进程之间交换位图数据时,它变得有用。
    请注意,有关 ImageLockMode.UserInputBuffer 的 MSDN 文档令人困惑(如果没有错的话)。

  • format As PixelFormat:用于描述单个像素颜色的格式。实际上,它转换为用于表示颜色的字节数。
    PixelFormat = Format24bppRgb时,每个Color用3个字节(RGB值)表示。使用 PixelFormat.Format32bppArgb,每种颜色由 4 个字节(RGB 值 + Alpha)表示。
    索引格式,如 Format8bppIndexed,指定每个字节值是 Palette 条目的索引。 Palette 是位图信息的一部分,像素格式为 PixelFormat.Indexed 时除外:在这​​种情况下,每个值都是系统颜色 table.
    中的一个条目 一个新的Bitmap对象默认的PixelFormat,如果没有指定,就是PixelFormat.Format32bppArgb,或者PixelFormat.Canonical

关于 Stride 的重要说明:

如前所述,Stride(也称为扫描线)表示组成单行像素的字节数。由于硬件对齐要求,它总是四舍五入到 4 字节边界(4 的整数倍)。

Stride =  [Bitmap Width] * [bytes per Color]
Stride += (Stride Mod 4) * [bytes per Color]

这就是为什么我们总是使用用 PixelFormat.Format32bppArgb 创建的位图的原因之一:位图的 Stride 总是已经与所需的边界对齐。

如果位图格式改为 PixelFormat.Format24bppRgb (每种颜色 3 个字节)?

如果位图的 Width 乘以每像素字节数不是 4 的倍数,则 Stride 将用 0 填充以填补空白.

大小为 (100 x 100) 的位图在 32 位和 24 位格式中都没有填充:

100 * 3 = 300 : 300 Mod 4 = 0 : Stride = 300
100 * 4 = 400 : 400 Mod 4 = 0 : Stride = 400

大小为(99 x 100)的位图会有所不同:

99 * 3 = 297 : 297 Mod 4 = 1 : Stride = 297 + ((297 Mod 4) * 3) = 300
99 * 4 = 396 : 396 Mod 4 = 0 : Stride = 396

24 位位图的 Stride 被填充添加 3 个字节(设置为 0)以填充边界。

当我们inspect/modify内部值通过坐标访问单个像素时,这不是问题,类似于SetPixel/GetPixel操作的方式:像素的位置总会被正确找到。

假设我们需要 inspect/change 在大小为 (99 x 100).
的位图中位置 (98, 70) 的像素 仅考虑每个像素的字节数。 Buffer内的像素位置为:

[Bitmap] = new Bitmap(99, 100, PixelFormat = Format24bppRgb)

[Bytes x pixel] = Image.GetPixelFormatSize([Bitmap].PixelFormat) / 8
[Pixel] = new Point(98, 70)
[Pixel Position] = ([Pixel].Y * [BitmapData.Stride]) + ([Pixel].X * [Bytes x pixel])
[Color] = Color.FromArgb([Pixel Position] + 2, [Pixel Position] + 1, [Pixel Position])

将像素的垂直位置乘以扫描线的宽度,缓冲区内的位置将始终正确:计算中包含填充的大小。
下一个位置 (0, 71) 的像素颜色将 return 预期结果:

顺序读取颜色字节时会有所不同
第一个扫描线将 return 有效结果直到最后一个像素(最后 3 个字节):接下来的 3 个字节将 return 用于舍入 Stride 的字节的值,所有设置为 0

这也可能不是问题。例如,应用一个过滤器,代表一个像素的每个字节序列都被读取并使用过滤器矩阵的值进行修改:我们将只修改一个 3 个字节的序列,在渲染位图时不会考虑这些字节。

但如果我们正在搜索特定的像素序列,这确实很重要:读取不存在的像素颜色可能会影响结果 and/or 使算法失衡。
对位图的颜色进行统计分析时也是如此。

当然,我们可以在循环中添加一个检查:if [Position] Mod [BitmapData].Width = 0 : continue.
但这会为每次迭代添加一个新的计算。

实际操作

简单的解决方案(更常见的一种)是创建格式为 PixelFormat.Format32bppArgb 的新位图,因此 Stride 将始终正确对齐:

Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Runtime.InteropServices

Private Function CopyTo32BitArgb(image As Image) As Bitmap
    Dim imageCopy As New Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb)
    imageCopy.SetResolution(image.HorizontalResolution, image.VerticalResolution)

    For Each propItem As PropertyItem In image.PropertyItems
        imageCopy.SetPropertyItem(propItem)
    Next

    Using g As Graphics = Graphics.FromImage(imageCopy)
        g.DrawImage(image,
            New Rectangle(0, 0, imageCopy.Width, imageCopy.Height),
            New Rectangle(0, 0, image.Width, image.Height),
            GraphicsUnit.Pixel)
        g.Flush()
    End Using
    Return imageCopy
End Function

这会生成具有相同 DPI 定义的字节兼容位图; Image.PropertyItems 也是从源图像复制的。

为了测试它,让我们对图像应用棕褐色调滤镜,使用它的副本执行对位图数据所需的所有修改:

Public Function BitmapFilterSepia(source As Image) As Bitmap
    Dim imageCopy As Bitmap = CopyTo32BitArgb(source)
    Dim imageData As BitmapData = imageCopy.LockBits(New Rectangle(0, 0, source.Width, source.Height),
        ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb)

    Dim buffer As Byte() = New Byte(Math.Abs(imageData.Stride) * imageCopy.Height - 1) {}
    Marshal.Copy(imageData.Scan0, buffer, 0, buffer.Length)

    Dim bytesPerPixel = Image.GetPixelFormatSize(source.PixelFormat) \ 8;
    Dim red As Single = 0, green As Single = 0, blue As Single = 0

    Dim pos As Integer = 0
    While pos < buffer.Length
        Dim color As Color = Color.FromArgb(BitConverter.ToInt32(buffer, pos))
        ' Dim h = color.GetHue()
        ' Dim s = color.GetSaturation()
        ' Dim l = color.GetBrightness()

        red = buffer(pos) * 0.189F + buffer(pos + 1) * 0.769F + buffer(pos + 2) * 0.393F
        green = buffer(pos) * 0.168F + buffer(pos + 1) * 0.686F + buffer(pos + 2) * 0.349F
        blue = buffer(pos) * 0.131F + buffer(pos + 1) * 0.534F + buffer(pos + 2) * 0.272F

        buffer(pos + 2) = CType(Math.Min(Byte.MaxValue, red), Byte)
        buffer(pos + 1) = CType(Math.Min(Byte.MaxValue, green), Byte)
        buffer(pos) = CType(Math.Min(Byte.MaxValue, blue), Byte)
        pos += bytesPerPixel
    End While

    Marshal.Copy(buffer, 0, imageData.Scan0, buffer.Length)
    imageCopy.UnlockBits(imageData)
    imageData = Nothing
    Return imageCopy
End Function

Bitmap.LockBits 不一定是 最佳 可用选择。
使用 ColorMatrix class 也可以很容易地执行应用过滤器的相同过程,它允许将 5x5 矩阵转换应用于位图,仅使用一个简单的浮点数组( Single) 值。

例如,让我们使用 ColorMatrix class 和众所周知的 5x5 矩阵应用灰度滤镜:

Public Function BitmapMatrixFilterGreyscale(source As Image) As Bitmap
    ' A copy of the original is not needed but maybe desirable anyway 
    ' Dim imageCopy As Bitmap = CopyTo32BitArgb(source)
    Dim filteredImage = New Bitmap(source.Width, source.Height, source.PixelFormat)
    filteredImage.SetResolution(source.HorizontalResolution, source.VerticalResolution)

    Dim grayscaleMatrix As New ColorMatrix(New Single()() {
        New Single() {0.2126F, 0.2126F, 0.2126F, 0, 0},
        New Single() {0.7152F, 0.7152F, 0.7152F, 0, 0},
        New Single() {0.0722F, 0.0722F, 0.0722F, 0, 0},
        New Single() {0, 0, 0, 1, 0},
        New Single() {0, 0, 0, 0, 1}
   })

    Using g As Graphics = Graphics.FromImage(filteredImage), attributes = New ImageAttributes()
        attributes.SetColorMatrix(grayscaleMatrix)
        g.DrawImage(source, New Rectangle(0, 0, source.Width, source.Height),
                    0, 0, source.Width, source.Height, GraphicsUnit.Pixel, attributes)
    End Using
    Return filteredImage
End Function