一行有 11 个像素(Width = 11), 对一个 32 位(每个像素 4 字节)的图像, Stride = 11 * 4 = 44
.
但还有个字节对齐的问题, 譬如:
一行有 11 个像素(Width = 11), 对一个 24 位(每个像素 3 字节)的图像, Stride = 11 * 3 + 3 = 36
.
为什么不是 Stride = 33
? 因为它是按 4 字节对齐的.
根据上面道理, 我们可以手动计算 Stride
的值:
1、Stride = 每像素占用的字节数(也就是像素位数/8) * Width;
2、如果 Stride 不是 4 的倍数, 那么 Stride = Stride + (4 - Stride mod 4);
下面的例子测试和验证了上面的思路:
uses GdiPlus;
procedure TForm1.FormPaint(Sender: TObject);
var
Bitmap: IGPBitmap;
Rect: TGPRect;
Graphics: IGPGraphics;
Data: TGPBitmapData;
n: Integer;
begin
ChDir('C:\GdiPlusImg\');
Bitmap := TGPBitmap.Create('Grapes.jpg');
Rect.Initialize(0, 0, Bitmap.Width, Bitmap.Height);
Data := Bitmap.LockBits(Rect, [ImageLockModeRead], Bitmap.PixelFormat);
n := GetPixelFormatSize(Data.PixelFormat) div 8 * Data.Width;
n := n + (4 - n mod 4);
ShowMessageFmt('%d, %d, %d', [Data.Width, Data.Stride, n]); { 187, 564, 564 }
Bitmap.UnlockBits(Data);
//
Bitmap := TGPBitmap.Create('Bird.bmp');
Rect.Initialize(0, 0, Bitmap.Width, Bitmap.Height);
Data := Bitmap.LockBits(Rect, [ImageLockModeRead], Bitmap.PixelFormat);
n := GetPixelFormatSize(Data.PixelFormat) div 8 * Data.Width;
if n mod 4 <> 0 then n := n + (4 - n mod 4);
ShowMessageFmt('%d, %d, %d', [Data.Width, Data.Stride, n]); { 110, 112, 112 }
Bitmap.UnlockBits(Data);
//
Bitmap := TGPBitmap.Create('Apple.gif');
Rect.Initialize(0, 0, Bitmap.Width, Bitmap.Height);
Data := Bitmap.LockBits(Rect, [ImageLockModeRead], Bitmap.PixelFormat);
n := GetPixelFormatSize(Data.PixelFormat) div 8 * Data.Width;
if n mod 4 <> 0 then n := n + (4 - n mod 4);
ShowMessageFmt('%d, %d, %d', [Data.Width, Data.Stride, n]); { 120, 120, 120 }
Bitmap.UnlockBits(Data);
//
Bitmap := TGPBitmap.Create('ImageFileSmall.jpg');
Rect.Initialize(0, 0, Bitmap.Width, Bitmap.Height);
Data := Bitmap.LockBits(Rect, [ImageLockModeRead], Bitmap.PixelFormat);
n := GetPixelFormatSize(Data.PixelFormat) div 8 * Data.Width;
if n mod 4 <> 0 then n := n + (4 - n mod 4);
ShowMessageFmt('%d, %d, %d', [Data.Width, Data.Stride, n]); { 320, 960, 960 }
Bitmap.UnlockBits(Data);
end;
使用C#
的BitmapData
最近要转开发平台,正研究C#
。C#
好是好,不过处理图片时一个像素一个像素的操作像素不是一般的慢。其实Delphi
也一样,但好在Delphi
的Bitmap
类提供了ScanLines
,可以一行一行的读图,效率比较高。C#
应该也有类似的东东。经过一番搜索,终于发现了BitmapData
类。
先看个例子,这是对一张位图的每个像素按FF
取补,然后输出到一个新图(代码有点啰嗦,不过应该可以说明问题了)。
int h = m_Bmp.Height;
int w = m_Bmp.Width;
Bitmap bmpOut = new Bitmap(w, h, PixelFormat.Format24bppRgb);
BitmapData dataIn = m_Bmp.LockBits(new Rectangle(0,0,w,h),ImageLockMode.ReadOnly,PixelFormat.Format24bppRgb;
BitmapData dataOut = bmpOut.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
unsafe
{
byte* pIn = (byte*)(dataIn.Scan0.ToPointer());
byte * pOut = (byte*)(dataOut.Scan0.ToPointer());
for (int y = 0; y < dataIn.Height; y++)
{
for (int x = 0; x < dataIn.Width; x++)
{
pOut[0] = (byte)(255 - pIn[0]);
pOut[1] = (byte)(255 - pIn[1]);
pOut[2] = (byte)(255 - pIn[2]);
pIn += 3;
pOut += 3;
}
pIn += dataIn.Stride - dataIn.Width * 3;
pOut += dataOut.Stride - dataOut.Width * 3;
}
}
bmpOut.UnlockBits(dataOut);
m_Bmp.UnlockBits(dataIn);
貌似比Delphi
复杂得多,难道我真的天生对指针过敏?还是Delphi
的比较好理解,就是扫描每一行,然后对当前像素点的三个分量做处理,非常方便。而且C#
代码中的Stride
是个什么东东?
查找了不少资料,现在我是这么理解的:
假设有一张图片宽度为6
,因为是Format24bppRgb
格式(每像素3
字节。在以下的讨论中,除非特别说明,否则Bitmap
都被认为是24位RGB)的,显然,每一行需要6*3=18
个字节存储。对于Bitmap
就是如此。但对于BitmapData
,虽然BitmapData.Width
还是等于Bitmap.Width
,但大概是出于显示性能的考虑,每行的实际的字节数将变成大于等于它的那个离它最近的4的整倍数,此时的实际字节数就是Stride
。就此例而言,18不是4的整倍数,而比18大的离18最近的4的倍数是20,所以这个BitmapData.Stride = 20
。显然,当宽度本身就是4的倍数时,BitmapData.Stride = Bitmap.Width * 3
。
画个图可能更好理解。R、G、B
分别代表3个原色分量字节,BGR
就表示一个像素。为了看起来方便我在每个像素之间插了个空格,实际上是没有的。X表示补足4的倍数而自动插入的字节。为了符合人类的阅读习惯我分行了,其实在计算机内存中应该看成连续的一大段。
Scan0
|
|———Stride—————–|
|———Width————-| | 注:Width是图片(BGR作为一个单位)宽度
BGR BGR BGR BGR BGR BGR XX
BGR BGR BGR BGR BGR BGR XX
.
.
.
现在应该很好理解了。首先用 BitmapData.Scan0
找到第0个像素的第0个分量的地址。这个地址指向的是个byte
类型,所以当时定义为byte* pIn
。
行扫描时,在当前指针位置(不妨看成当前像素的第0个颜色分量)连续取出三个值(3个原色分量。注意,0 1 2代表的次序是B G R
。在取指针指向的值时,貌似p[n]
和p += n
再取p[0]
是等价的),然后下移3个位置(pIn += 3
,看成指到下一个像素的第0个颜色分量)。做过Bitmap.Width
次操作后,就到达了Bitmap.Width * 3
的位置,应该要跳过图中标记为X的字节了(共有Stride - Width * 3
个字节),代码中就是 pIn += dataIn.Stride - dataIn.Width * 3;
跳过以后指针就到达下行的第0个像素了。按照此算法,一共需要做Bitmap.Height
次行扫描(代码就是 for (int y = 0; y < dataIn.Height; y++)
)。
另外,因为使用了unsafe
,所以编译的时候需要设置“允许不安全的代码”。