分析图像的颜色

Posted

技术标签:

【中文标题】分析图像的颜色【英文标题】:Analyze colors of an Image 【发布时间】:2020-03-23 20:30:14 【问题描述】:

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

我需要分析图像中有多少像素属于每个指定的颜色范围。 基于精度/速度滑块,我跳过了一些行/像素。

它运行良好,但速度太慢。高精度(trackbar value = 1),大约需要550毫秒。 精度低但速度快(轨迹条值 = 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)

【问题讨论】:

@sharkyenergy 你应该使用AndAlso 而不是And,因为它可以更快:What is the difference between And and AndAlso in VB.NET? 【参考方案1】:

当对位图的颜色数据执行顺序操作时,Bitmap.LockBits 方法可以提供巨大的性能提升,因为位图数据只需加载到内存中一次,而不是顺序 GetPixel/SetPixel 调用:每次调用将在内存中加载部分位图数据,然后将其丢弃,以在再次调用这些方法时重复该过程。

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

format As PixelFormat:用于描述单个像素颜色的格式。实际上,它转换为用于表示颜色的字节数。 当PixelFormat = Format24bppRgb 时,每种颜色由 3 个字节(RGB 值)表示。使用PixelFormat.Format32bppArgb,每种颜色由 4 个字节(RGB 值 + Alpha)表示。 索引格式,如Format8bppIndexed,指定每个字节值是Palette 条目的索引。 Palette 是位图信息的一部分,除非像素格式为 PixelFormat.Indexed:在这种情况下,每个值都是系统颜色表中的一个条目。 新位图对象的默认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 将用0s 填充以填补空白。

大小为(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)以填充边界。

当我们检查/修改通过坐标访问单个像素的内部值时,这不是一个问题,类似于 SetPixel/GetPixel 的操作方式:始终可以正确找到像素的位置。

假设我们需要在大小为(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),将返回预期的结果:

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

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

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

当然,我们可以在循环中添加一个检查: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 类也可以很容易地执行应用过滤器的相同过程,它允许将5x5 矩阵变换应用于位图,只使用一个简单的浮点数组(Single) 值。

例如,让我们使用ColorMatrix 类和众所周知的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

【讨论】:

"bytes per color" 仅适用于每像素格式不低于 8 位的情况。更通用的步幅计算是(((((bitsPerPixel * width) + 7) / 8) + 3) / 4) * 4 @Nyerguds 这些注释与 24 位和 32 位图像非常明确相关(非常明确地建议单独使用后者)。没有处理索引颜色,也没有处理 16 位灰度等格式。关于这些,我在这里发布了一个示例类来分析它们:How to determine if an Image is Grayscale。顺便说一句,如果你看看它并且你有一些关于它的 注释,不要避免评论,我喜欢你的 cmets :)

以上是关于分析图像的颜色的主要内容,如果未能解决你的问题,请参考以下文章

图像特征分析:颜色特征描述,颜色矩,颜色直方图(附matlab代码)

图像特征分析:颜色特征描述,颜色矩,颜色直方图(附matlab代码)

图像特征分析:颜色特征描述,颜色矩,颜色直方图(附matlab代码)

OpenCV图像颜色分析

VOC Segmentation GT图像颜色表生成分析

电影口碑与海报图像的相关性分析