閱讀提示:
《Delphi影像處理》系列以效率為側重點,一般代碼為PASCAL,核心代碼採用BASM。
《C++影像處理》系列以代碼清晰,可讀性為主,全部使用C++代碼。
儘可能保持二者內容一致,可相互對照。
本文代碼必須包括文章《Delphi影像處理 -- 資料類型及公用過程》中的ImageData.pas單元和《Delphi影像處理 -- 平面幾何變換類》TransformMatrix.pas單元。
在《Delphi影像處理 -- 平面幾何變換類》一文中,介紹了映像平面幾何變換類TTransformMatrix,並寫了一個簡單的臨近插值法映像幾何變換函數Transform,用於測試。很顯然,Transform函數產生的變換映像不僅品質較差,而且也不具備通用性,只能作為一個實現映像幾何變換的架構。
本文擬採用臨近插值法、雙線性插值法和雙立方插值法等三種插值方式,來實現較完整、通用的圖形映像平面幾何變換。三種插值過程程式碼封裝含在《Delphi影像處理 -- 平面幾何變換類》一文中,本文代碼不包括TTransformMatrix類和三種插值過程。
procedure SetMixerMM;asm pxor mm7, mm7 mov eax, 1011h movd mm6, eax pshufw mm6, mm6, 0 mov eax, 8 movd mm5, eax pshufw mm5, mm5, 0end;procedure MixerColor;asm cmp eax, 255 jb @@ArgbMix movd [edi], xmm0 ret@@ArgbMix: movdq2q mm0, xmm0 movd mm1, [edi] punpcklbw mm0, mm7 punpcklbw mm1, mm7 pshufw mm2, mm0, 255 movzx eax, [edi].TARGBQuad.Alpha cmp eax, 255 jne @@PArgbMix // dest.argb = dest.argb + source.argb - dest.argb * source.alpha / 255 paddw mm0, mm1 pmullw mm1, mm2 pmulhuw mm1, mm6 paddusw mm1, mm5 psrlw mm1, 4 psubsw mm0, mm1 packuswb mm0, mm0 movd [edi], mm0 ret@@PArgbMix: // dest.rgb = dest.rgb * dest.alpha / 255 pshufw mm3, mm1, 255 pmullw mm1, mm3 pmulhuw mm1, mm6 paddusw mm1, mm5 psrlw mm1, 4 pinsrw mm1, eax, 3 // dest.argb = dest.argb + source.argb - dest.argb * sourec.alpha / 255 paddw mm0, mm1 pmullw mm1, mm2 pmulhuw mm1, mm6 paddusw mm1, mm5 psrlw mm1, 4 psubsw mm0, mm1 pextrw eax, mm0, 3 // dest.rgb = dest.rgb * 255 / dest.alpha movq mm1, mm0 psllw mm0, 8 psubw mm0, mm1 pmulhuw mm0, qword ptr MMDivTab[eax*8] packuswb mm0, mm7 movd [edi], mm0 mov [edi].TARGBQuad.Alpha, alend;procedure CopyInterpolateData(var Deat: TImageData; const Source: TImageData; Alpha: Integer);asm push esi push edi push ebx cmp ecx, 256 jne @@CvtPArgb cmp [edx].TImageData.AlphaFlag, True je @@CvtPArgb call _SetCopyRegs@@CpyLoop: push ecx rep movsd pop ecx add esi, eax add edi, ebx dec edx jne @@CpyLoop jmp @@Exit@@CvtPArgb: push ebp push ecx call _SetCopyRegs mov ebp, eax call SetMixerMM pop eax movd mm2, eax pshufw mm2, mm2, 0@@yLoop: push ecx@@xLoop: movd mm0, [esi] punpcklbw mm0, mm7 pshufw mm1, mm0, 255 pmullw mm1, mm2 psrlw mm1, 8 // alpha0 = Source.Alpha * Alpha / 256 pmullw mm0, mm1 // Source.RGB = (Source.RGB * alpha0 + 127) / 255 pmulhuw mm0, mm6 paddusw mm0, mm5 psrlw mm0, 4 packuswb mm0, mm7 movd eax, mm1 movd [edi], mm0 mov [edi].TARGBQuad.Alpha, al add esi, 4 add edi, 4 loop @@xLoop add esi, ebp add edi, ebx pop ecx dec edx jnz @@yLoop pop ebp emms@@Exit: pop ebx pop edi pop esiend;procedure FillBorder(var Data: TImageData; Radius: Integer; FillFlag: Integer);asm push ebp push esi push edi push ebx test ecx, 0fffh jne @@yFill push eax push ecx mov edi, [eax].TImageData.Stride imul edi, edx add edi, [eax].TImageData.Scan0 mov ebp, [eax].TImageData.Width mov ebx, [eax].TImageData.Height mov esi, edx shl esi, 1 sub ebx, esi sub ebp, esi shl ebp, 2 shl esi, 1@@cLoop: mov eax, [edi+esi] mov ecx, edx rep stosd add edi, ebp mov eax, [edi-4] mov ecx, edx rep stosd dec ebx jnz @@cLoop pop ecx pop eax@@yFill: test ecx, 0fff0000h jne @@Exit mov ebp, [eax].TImageData.Width mov edi, [eax].TImageData.Scan0 mov esi, [eax].TImageData.Stride imul esi, edx add esi, edi push edx@@tLoop: push esi mov ecx, ebp rep movsd pop esi dec edx jnz @@tLoop pop edx mov ebx, [eax].TImageData.Height sub ebx, edx sub ebx, edx imul ebx, [eax].TImageData.Stride add edi, ebx mov esi, edi sub esi, [eax].TImageData.Stride@@bLoop: mov ecx, ebp rep movsd dec edx jnz @@bLoop@@Exit: pop ebx pop edi pop esi pop ebpend;type TInterpolateProc = procedure; TElementsI = array[0..5] of Integer;procedure _DoTransform(var Dest: TImageData; const Soutce: TImageData; const Elements: TElementsI; Radius: Integer; ipProc: TInterpolateProc);var im22, im21, im12, im11: Integer; up, xDown, yDown: Integer; width, height, dstOffset: Integer; scan0: Pointer;asm push esi push edi push ebx mov ebx, [ecx] mov im11, ebx mov ebx, [ecx+4] mov im12, ebx mov ebx, [ecx+8] mov im21, ebx mov ebx, [ecx+12] mov im22, ebx push dword ptr[ecx+16] push dword ptr[ecx+20] mov esi, Radius mov ebx, [edx].TImageData.Width mov edi, [edx].TImageData.Height sub ebx, esi sub edi, esi shl ebx, 12 shl edi, 12 mov xDown, ebx // xDown = (Source.Width - radius) * 4096 mov yDown, edi // yDown = (Source.Height - radius) * 4096 mov ebx, [eax].TImageData.Height mov height, ebx // height = dest.Height mov ebx, [eax].TImageData.Width mov width, ebx // width = dest.Width shl ebx, 2 neg ebx add ebx, [eax].TImageData.Stride mov dstOffset, ebx // dstOffset = dest.Stride - dest.Width * 4 mov edi, [eax].TImageData.Scan0 mov ebx, [edx].TImageData.Scan0 mov scan0, ebx mov ebx, [edx].TImageData.Stride pop ecx // ys pop edx // xs shl esi, 11 mov up, esi // up = radius * 2048 add esi, 800h add edx, esi // xs += (up + 2048) add ecx, esi // ys += (up + 2048) pxor xmm7, xmm7 mov eax, 40004h movd xmm6, eax pshufd xmm6, xmm6, 0 call SetMixerMM@@yLoop: push ecx // for (pd = (ARGB*)Dest.Scan0; height > 0; height --) push edx // { push width // x = xs@@xLoop: // y = ys cmp ecx, up // for (w = width; w > 0; w --) jl @@Next // { cmp ecx, yDown jge @@Next cmp edx, up jl @@Next cmp edx, xDown // if (y >= up && y < yDown && x >= up && x < xDown) jge @@Next // { mov esi, ecx // y0 = y / 4096 mov eax, edx // x0 = x / 4096 sar esi, 12 sar eax, 12 imul esi, ebx shl eax, 2 add esi, eax add esi, scan0 // esi = Source.Scan0 + x0 * 4 + y0 * Source.Stride call ipProc movd eax, xmm0 // xmm0 = eax = ipProc(src, x, y, esi) shr eax, 24 jz @@Next call MixerColor // if (eax >> 24) MixerColor(pd, mm0, eax)@@Next: add edi, 4 // pd ++ add edx, im11 // x += im11 add ecx, im12 // y += im12 dec width // } jnz @@xLoop pop width pop edx pop ecx add edi, dstOffset // (LPBYTE)pd += dstOffset add edx, im21 // xs += im21 add ecx, im22 // ys += im22 dec height // } jnz @@yLoop emms@@Exit: pop ebx pop edi pop esiend;// 擷取插值過程和擴充半徑function GetInterpolateProc(const Data: TImageData; var Proc: TInterpolateProc): Integer;const procs: array[TInterpolateMode] of TInterpolateProc = (_GetBilinearColor, _GetNearColor, _GetBilinearColor, _GetBicubicColor); radius: array[TInterpolateMode] of Integer = (2, 1, 2, 4);begin Proc := procs[Data.IpMode]; Result := radius[Data.IpMode];end;// 按Matrix計算目標和源映像幾何變換的裁剪矩形function _GetTransformParams(dstWidth, dstHeight, srcWidth, srcHeight: Integer; var Matrix: TTransformMatrix; var dst, src: TRect): Boolean; function CalcRectI(Width, height: Integer; var r: TRect): Boolean; begin Inc(r.Right, r.Left); Inc(r.Bottom, r.Top); if r.Right > Width then r.Right := Width; if r.Bottom > Height then r.Bottom := Height; if r.Left > 0 then Dec(r.Right, r.Left) else r.Left := 0; if r.Top > 0 then Dec(r.Bottom, r.Top) else r.Top := 0; Result := (r.Right > 0) and (r.Bottom > 0); end;var fx, fy, fwidth, fheight: Single;begin Matrix.GetTransformSize(srcWidth, srcHeight, fx, fy, fwidth, fheight); Matrix.Invert; dst := Rect(Trunc(fx), Trunc(fy), _Infinity(fwidth), _Infinity(fheight)); Result := CalcRectI(dstWidth, dstHeight, dst); if not Result then Exit; if (fx > 0) or (fy > 0) then begin if fx < 0 then fx := 0 else if (fy < 0) then fy := 0; Matrix.Translate(fx, fy); end; Matrix.GetTransformSize(dst.Right, dst.Bottom, fx, fy, fwidth, fheight); src := Rect(Trunc(fx), Trunc(fy), _Infinity(fwidth), _Infinity(fheight)); Result := CalcRectI(srcWidth, srcHeight, src); if not Result then Exit; if fx > 0 then Matrix.OffsetX := Matrix.OffsetX - fx; if fy > 0 then Matrix.OffsetY := Matrix.OffsetY - fy;end;procedure ImageTransform(var Dest: TImageData; x, y: Integer; const Source: TImageData; const Matrix: TTransformMatrix; Alpha: Single = 1);var m: TTransformMatrix; e: TMatrixElements; eI: TElementsI; dstR, srcR: TRect; i, alphaI, radius: Integer; proc: TInterpolateProc; dst, src, tmp, sub: TImageData;begin alphaI := Round(Alpha * 256); if alphaI <= 0 then Exit; if alphaI > 256 then alphaI := 256; m := TTransformMatrix.Create(Matrix); try m.OffsetX := m.OffsetX + x; m.OffsetY := m.OffsetY + y; if not _GetTransformParams(Dest.Width, Dest.Height, Source.Width, Source.Height, m, dstR, srcR) then Exit; e := m.Elements; for i := 0 to 5 do eI[i] := Round(e.Elements[i] * 4096); radius := GetInterpolateProc(Source, proc); dst := GetSubImageData(Dest, dstR.Left, dstR.Top, dstR.Right, dstR.Bottom); tmp := GetSubImageData(Source, srcR.Left, srcR.Top, srcR.Right, srcR.Bottom); src := NewImageData(tmp.Width + radius * 2, tmp.Height + radius * 2); try src.AlphaFlag := Source.AlphaFlag; sub := GetSubImageData(src, radius, radius, tmp.Width, tmp.Height); CopyInterpolateData(sub, tmp, alphaI); FillBorder(src, radius, (eI[1] shl 16) or (eI[2] and $ffff)); _DoTransform(dst, src, eI, radius, proc); if (eI[1] = 0) and (eI[2] = 0) and (alphaI = 256) and not Source.AlphaFlag and (Dest.Width = dst.Width) and (Dest.Height = dst.Height) then Dest.AlphaFlag := False; finally FreeImageData(src); end; finally m.Free; end;end;
代碼很長,但核心代碼還是_DoTransform過程和《Delphi影像處理 -- 平面幾何變換類》中的三個插值過程。幾何變換過程原理亦可參見該文。
有關插值邊界的處理,一般有2種辦法,一是在插值過程中進行判斷座標是否超界而作相應的處理,二是捨棄邊界部分,對於後者我是不主張的,因為那樣是不完整的處理。我採用了擴充邊框的辦法進行邊框插值處理,這樣一來,雖然多了一道拷貝過程,卻少了具體插值過程的座標判斷,二者抵消,插值速度應該是差不多的(據我測試,擴充邊框辦法在映像放大和旋轉處理中速度還是略快一些),但是簡化了插值代碼。
下面是映像幾何變換的例子:
procedure TForm1.Button1Click(Sender: TObject);var bmp, newBmp: TBitmap; jpg: TJPEGImage; matrix: TTransformMatrix; source, dest: TImageData; r: TRect;begin bmp := TBitmap.Create; matrix := TTransformMatrix.Create; try jpg := TJPEGImage.Create; try jpg.LoadFromFile('..\..\media\IMG_9440_mf.jpg'); bmp.Assign(jpg); finally jpg.Free; end; matrix.Scale(1.2, 1.2); matrix.Shear(0.3, 0.3); r := matrix.GetTransformRect(bmp.Width, bmp.Height); if (r.Right <= 0) or (r.Bottom <= 0) then Exit; source := GetBitmapData(bmp); newBmp := TBitmap.Create; try newBmp.PixelFormat := pf32bit; newBmp.Width := r.Right; newBmp.Height := r.Bottom; dest := GetBitmapData(newBmp); SetInterpolateMode(source, imBicubic); ImageTransform(dest, 0, 0, source, matrix); Canvas.Draw(0, 0, newBmp); finally newBmp.Free; end; finally matrix.Free; bmp.Free; end;end;
代碼中的SetInterpolateMode(source, imBicubic);語句為設定雙立方插值方式,該函數也在《Delphi影像處理 -- 資料類型及公用過程》的ImageData.pas單元。
運行效果如下:
運行效果還是很好的,特別是邊界效果。
對於含Alpha資訊的映像,必須作自乘預先處理,這在CopyAlphaData函數中是自動判斷後轉換的。比較下面同一張png圖片2種方式的線性插值幾何變換處理效果,左邊是使用自乘預先處理後的插值處理效果,右邊是沒做自乘預先處理後的插值處理效果:
《Delphi影像處理》系列使用GDI+單元和說明見文章《GDI+ for VCL基礎 -- GDI+ 與 VCL》。
因水平有限,錯誤在所難免,歡迎指正和指導。郵箱地址:maozefa@hotmail.com
這裡可訪問《Delphi影像處理 -- 文章索引》。