在 上一篇文章里,我们讲解了图像的垂直翻转(FlipY)算法,于是本文来探讨水平翻转(FlipX)。先讲解比较容易的32位图像水平翻转算法,便于后续文章来探讨复杂的24位图像水平翻转算法。
本文除了会给出标量算法外,还会给出向量算法。且这些算法是跨平台的,同一份源代码,能在 X86(Sse、Avx等指令集)及Arm(AdvSimd等指令集)等架构上运行,且均享有SIMD硬件加速。
一、标量算法
1.1 算法思路
水平翻转又称左右翻转,是将图像沿着垂直中轴线进行翻转。
假设用 src[x, y]
可以访问源图像中的像素,用 dst[x, y]
可以访问目标图像中的像素,width是图像的像素宽度。那么水平翻转的公式为——
dst[x, y] = src[width - 1 - x, y]
注意像素坐标是从0开始编号的。于是最右边像素的x坐标是 width - 1
。
简单来说,就是将行内的每一个像素,按相反的方向复制一遍。
由于需要逐个逐个的处理每一个像素,所以得根据不同的像素大小来编写算法。
1.1.1 32位像素的说明
32位像素是容易处理的。因为32位就是4字节,这是2的整数次幂,处理起来很方便。所以32位图像的使用频率最高。
受到 RGB通道顺序、是否含有 Alpha通道 等细节的影响,32位的像素有很多种像素格式——
- Bgr32。又称 BGRX8888、B8G8R8X8。GDI+ 里称 “Format32bppRgb”。
- Bgra32。又称 BGRA8888、B8G8R8A8。GDI+ 里称“Format32bppArgb”。
- Pbgra32。又称 预乘的BGRA8888、B8G8R8A8。GDI+ 里称“Format32bppPArgb”。
- Rgb32。又称 RGBX8888、R8G8B8X8。
- Rgba32。又称 RGBA8888、R8G8B8A8。
- Prgba32。又称 预乘的RGBA8888、R8G8B8A8。
由于现在是做图像水平翻转,无需精确到颜色通道,而是可以将整个像素作为整体进行处理。所以,本文的算法对所有的32位像素格式都有效,不仅上面提到的Bgr32等格式,其实连 Cmyk32等其他32位像素也有效。
1.2 算法实现
知道像素的字节数(cbPixel)后,便可以根据它来复制像素了。32位,就是4个字节。
源代码如下。
public static unsafe void ScalarDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 4; // 32 bit: Bgr32, Bgra32, Rgb32, Rgba32.
byte* pRow = pSrc;
byte* qRow = pDst;
for (int i = 0; i < height; i++) {
byte* p = pRow + (width - 1) * cbPixel;
byte* q = qRow;
for (int j = 0; j < width; j++) {
for (int k = 0; k < cbPixel; k++) {
q[k] = p[k];
}
p -= cbPixel;
q += cbPixel;
}
pRow += strideSrc;
qRow += strideDst;
}
}
用指针来编写图像的水平翻转算法,最关键的是做好地址计算。现在是水平翻转,故重点是做好行内像素(内循环j)相关的地址计算。
内循环采用了“逆序读取、顺序写入”的策略。具体来说——
- 读取是从最后像素开始的,每次循环后移动到前一个像素。于是在上面的源代码中,p的初值是
pRow + (width - 1) * cbPixel
(目标位图最后一行的地址),每次循环后q会 减去 cbPixel。 - 写入是从第0个像素开始的,每次循环后移动到下一个像素。于是在上面的源代码中,q的初值就是
qRow
,每次循环后q会 加上 cbPixel。
1.3 基准测试代码
使用 BenchmarkDotNet 进行基准测试。
[Benchmark(Baseline = true)]
public void Scalar() {
ScalarDo(_sourceBitmapData, _destinationBitmapData, false);
}
//[Benchmark]
public void ScalarParallel() {
ScalarDo(_sourceBitmapData, _destinationBitmapData, true);
}
public static unsafe void ScalarDo(BitmapData src, BitmapData dst, bool useParallel = false) {
int width = src.Width;
int height = src.Height;
int strideSrc = src.Stride;
int strideDst = dst.Stride;
byte* pSrc = (byte*)src.Scan0.ToPointer();
byte* pDst = (byte*)dst.Scan0.ToPointer();
bool allowParallel = useParallel && (height > 16) && (Environment.ProcessorCount > 1);
if (allowParallel) {
Parallel.For(0, height, i => {
int start = i;
int len = 1;
byte* pSrc2 = pSrc + start * (long)strideSrc;
byte* pDst2 = pDst + start * (long)strideDst;
ScalarDoBatch(pSrc2, strideSrc, width, len, pDst2, strideDst);