unit Img32.Transform; (******************************************************************************* * Author : Angus Johnson * * Version : 4.7 * * Date : 6 January 2025 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2019-2025 * * Purpose : Affine and projective transformation routines for TImage32 * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface {$I Img32.inc} uses SysUtils, Classes, Math, Types, Img32, Img32.Vector; type TMatrixD = array [0..2, 0..2] of double; //Matrix functions function IsIdentityMatrix(const matrix: TMatrixD): Boolean; {$IFDEF INLINE} inline; {$ENDIF} function IsValidMatrix(const matrix: TMatrixD): Boolean; {$IFDEF INLINE} inline; {$ENDIF} function Matrix(const m00, m01, m02, m10, m11, m12, m20, m21, m22: double): TMatrixD; function MatrixDeterminant(const matrix: TMatrixD): double; function MatrixAdjugate(const matrix: TMatrixD): TMatrixD; function MatrixInvert(var matrix: TMatrixD): Boolean; // Note: Matrix multiplication IS NOT commutative hence ... procedure MatrixMultiply(var matrix1: TMatrixD; const matrix2: TMatrixD); procedure MatrixMultiply2(const matrix1: TMatrixD; var matrix2: TMatrixD); procedure MatrixApply(const matrix: TMatrixD; var x, y: double); overload; {$IFDEF INLINE} inline; {$ENDIF} procedure MatrixApply(const matrix: TMatrixD; var pt: TPointD); overload; {$IFDEF INLINE} inline; {$ENDIF} procedure MatrixApply(const matrix: TMatrixD; var rec: TRect); overload; procedure MatrixApply(const matrix: TMatrixD; var rec: TRectD); overload; procedure MatrixApply(const matrix: TMatrixD; var path: TPathD); overload; procedure MatrixApply(const matrix: TMatrixD; var paths: TPathsD); overload; procedure MatrixApply(const matrix: TMatrixD; img: TImage32; scaleAdjust: Boolean = false); overload; {$IFDEF INLINE} inline; {$ENDIF} procedure MatrixApply(const matrix: TMatrixD; img, targetImg: TImage32; scaleAdjust: Boolean = false); overload; {$IFDEF INLINE} inline; {$ENDIF} procedure MatrixSkew(var matrix: TMatrixD; angleX, angleY: double); procedure MatrixScale(var matrix: TMatrixD; scale: double); overload; procedure MatrixScale(var matrix: TMatrixD; scaleX, scaleY: double); overload; procedure MatrixRotate(var matrix: TMatrixD; angRad: double); overload; procedure MatrixRotate(var matrix: TMatrixD; const center: TPointD; angRad: double); overload; procedure MatrixTranslate(var matrix: TMatrixD; dx, dy: double); // The following MatrixExtract routines assume here is no skew procedure MatrixExtractScale(const mat: TMatrixD; out scale: double); overload; procedure MatrixExtractScale(const mat: TMatrixD; out X, Y: double); overload; procedure MatrixExtractTranslation(const mat: TMatrixD; out dx, dy: double); procedure MatrixExtractRotation(const mat: TMatrixD; out angle: double); // MatrixExtractAll - except skew :) function MatrixExtractAll(const mat: TMatrixD; out angle: double; out scale, trans: TPointD): Boolean; // AffineTransformImage: will automagically translate the image // Note: when the "scaleAdjust" parameter is enabled, it prevents antialiasing // from extending way outside of images when they are being enlarged // significantly (> 2 times) and rotated concurrently function AffineTransformImage(img: TImage32; matrix: TMatrixD; scaleAdjust: Boolean = false): TPoint; overload; {$IFDEF INLINE} inline; {$ENDIF} function AffineTransformImage(img, targetImg: TImage32; matrix: TMatrixD; scaleAdjust: Boolean = false): TPoint; overload; // ProjectiveTransform: // srcPts, dstPts => each path must contain 4 points // margins => the margins around dstPts (in the dest. projective). // Margins are only meaningful when srcPts are inside the image. function ProjectiveTransform(img: TImage32; const srcPts, dstPts: TPathD; const margins: TRect): Boolean; overload; {$IFDEF INLINE} inline; {$ENDIF} function ProjectiveTransform(img, targetImg: TImage32; const srcPts, dstPts: TPathD; const margins: TRect): Boolean; overload; function SplineVertTransform(img: TImage32; const topSpline: TPathD; splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean; overload; {$IFDEF INLINE} inline; {$ENDIF} function SplineVertTransform(img, targetImg: TImage32; const topSpline: TPathD; splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean; overload; function SplineHorzTransform(img: TImage32; const leftSpline: TPathD; splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean; overload; {$IFDEF INLINE} inline; {$ENDIF} function SplineHorzTransform(img, targetImg: TImage32; const leftSpline: TPathD; splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean; overload; type PWeightedColor = ^TWeightedColor; TWeightedColor = {$IFDEF RECORD_METHODS} record {$ELSE} object {$ENDIF} private fAddCount : Integer; fAlphaTot : Int64; fColorTotR: Int64; fColorTotG: Int64; fColorTotB: Int64; function GetColor: TColor32; public procedure Reset; overload; {$IFDEF INLINE} inline; {$ENDIF} procedure Reset(c: TColor32; w: Integer = 1); overload; {$IFDEF INLINE} inline; {$ENDIF} procedure Add(c: TColor32; w: Integer); overload; {$IFDEF INLINE_COMPATIBLE} inline; {$ENDIF} procedure Add(c: TColor32); overload; {$IFDEF INLINE} inline; {$ENDIF} procedure Add(const other: TWeightedColor); overload; {$IFDEF INLINE} inline; {$ENDIF} procedure Subtract(c: TColor32; w: Integer); overload; procedure Subtract(c: TColor32); overload; {$IFDEF INLINE} inline; {$ENDIF} procedure Subtract(const other: TWeightedColor); overload; {$IFDEF INLINE} inline; {$ENDIF} function AddSubtract(addC, subC: TColor32): Boolean; {$IFDEF INLINE_COMPATIBLE} inline; {$ENDIF} function AddNoneSubtract(c: TColor32): Boolean; {$IFDEF INLINE_COMPATIBLE} inline; {$ENDIF} procedure AddWeight(w: Integer); {$IFDEF INLINE} inline; {$ENDIF} property AddCount: Integer read fAddCount; property Color: TColor32 read GetColor; property Weight: integer read fAddCount; end; TArrayOfWeightedColor = array of TWeightedColor; const IdentityMatrix: TMatrixD = ((1, 0, 0),(0, 1, 0),(0, 0, 1)); implementation uses Img32.Resamplers; resourcestring rsInvalidScale = 'Invalid matrix scaling factor (0)'; const DivOneByXTableSize = 1024; {$IFDEF CPUX86} // Use faster Trunc for x86 code in this unit. Trunc: function(Value: Double): Integer = __Trunc; {$ENDIF CPUX86} var // DivOneByXTable[x] = 1/x DivOneByXTable: array[0 .. DivOneByXTableSize -1] of Double; //------------------------------------------------------------------------------ // Matrix functions //------------------------------------------------------------------------------ function IsIdentityMatrix(const matrix: TMatrixD): Boolean; begin result := (matrix[0,0] = 1) and (matrix[0,1] = 0) and (matrix[0,2] = 0) and (matrix[1,0] = 0) and (matrix[1,1] = 1) and (matrix[1,2] = 0) and (matrix[2,0] = 0) and (matrix[2,1] = 0) and (matrix[2,2] = 1); end; //------------------------------------------------------------------------------ function IsValidMatrix(const matrix: TMatrixD): Boolean; begin result := matrix[2][2] = 1.0; end; //------------------------------------------------------------------------------ function Matrix(const m00, m01, m02, m10, m11, m12, m20, m21, m22: double): TMatrixD; begin Result[0,0] := m00; Result[0,1] := m01; Result[0,2] := m02; Result[1,0] := m10; Result[1,1] := m11; Result[1,2] := m12; Result[2,0] := m20; Result[2,1] := m21; Result[2,2] := m22; end; //------------------------------------------------------------------------------ function Det4(a1, a2, b1, b2: double): double; {$IFDEF INLINE} inline; {$ENDIF} begin Result := a1 * b2 - a2 * b1; end; //------------------------------------------------------------------------------ function Det9(a1, a2, a3, b1, b2, b3, c1, c2, c3: double): double; {$IFDEF INLINE} inline; {$ENDIF} begin Result := a1 * Det4(b2, b3, c2, c3) - b1 * Det4(a2, a3, c2, c3) + c1 * Det4(a2, a3, b2, b3); end; //------------------------------------------------------------------------------ function MatrixDeterminant(const matrix: TMatrixD): double; {$IFDEF INLINE} inline; {$ENDIF} begin Result := Det9(matrix[0,0], matrix[1,0], matrix[2,0], matrix[0,1], matrix[1,1], matrix[2,1], matrix[0,2], matrix[1,2], matrix[2,2]); end; //------------------------------------------------------------------------------ function MatrixAdjugate(const matrix: TMatrixD): TMatrixD; begin //https://en.wikipedia.org/wiki/Adjugate_matrix Result[0,0] := Det4(matrix[1,1], matrix[1,2], matrix[2,1], matrix[2,2]); Result[0,1] := -Det4(matrix[0,1], matrix[0,2], matrix[2,1], matrix[2,2]); Result[0,2] := Det4(matrix[0,1], matrix[0,2], matrix[1,1], matrix[1,2]); Result[1,0] := -Det4(matrix[1,0], matrix[1,2], matrix[2,0], matrix[2,2]); Result[1,1] := Det4(matrix[0,0], matrix[0,2], matrix[2,0], matrix[2,2]); Result[1,2] := -Det4(matrix[0,0], matrix[0,2], matrix[1,0], matrix[1,2]); Result[2,0] := Det4(matrix[1,0], matrix[1,1], matrix[2,0], matrix[2,1]); Result[2,1] := -Det4(matrix[0,0], matrix[0,1], matrix[2,0], matrix[2,1]); Result[2,2] := Det4(matrix[0,0], matrix[0,1], matrix[1,0], matrix[1,1]); end; //------------------------------------------------------------------------------ procedure MatrixApply(const matrix: TMatrixD; var x, y: double); var tmpX: double; begin tmpX := x; x := tmpX * matrix[0, 0] + y * matrix[1, 0] + matrix[2, 0]; y := tmpX * matrix[0, 1] + y * matrix[1, 1] + matrix[2, 1]; end; //------------------------------------------------------------------------------ procedure MatrixApply(const matrix: TMatrixD; var pt: TPointD); var tmpX: double; begin tmpX := pt.x; pt.X := tmpX * matrix[0, 0] + pt.Y * matrix[1, 0] + matrix[2, 0]; pt.Y := tmpX * matrix[0, 1] + pt.Y * matrix[1, 1] + matrix[2, 1]; end; //------------------------------------------------------------------------------ procedure MatrixApply(const matrix: TMatrixD; var rec: TRect); var path: TPathD; begin if not IsValidMatrix(matrix) then Exit; path := Rectangle(rec); MatrixApply(matrix, path); rec := GetBounds(path); end; //------------------------------------------------------------------------------ procedure MatrixApply(const matrix: TMatrixD; var rec: TRectD); var path: TPathD; begin if not IsValidMatrix(matrix) then Exit; path := Rectangle(rec); MatrixApply(matrix, path); rec := GetBoundsD(path); end; //------------------------------------------------------------------------------ procedure MatrixApply(const matrix: TMatrixD; var path: TPathD); var i, len: integer; tmpX: double; pp: PPointD; begin len := Length(path); if (len = 0) or IsIdentityMatrix(matrix) or not IsValidMatrix(matrix) then Exit; pp := @path[0]; for i := 0 to len -1 do begin tmpX := pp.X; pp.X := tmpX * matrix[0, 0] + pp.Y * matrix[1, 0] + matrix[2, 0]; pp.Y := tmpX * matrix[0, 1] + pp.Y * matrix[1, 1] + matrix[2, 1]; inc(pp); end; end; //------------------------------------------------------------------------------ procedure MatrixApply(const matrix: TMatrixD; var paths: TPathsD); var i,j,len: integer; tmpX: double; pp: PPointD; begin if not Assigned(paths) or IsIdentityMatrix(matrix) or not IsValidMatrix(matrix) then Exit; for i := 0 to High(paths) do begin len := Length(paths[i]); if len = 0 then Continue; pp := @paths[i][0]; for j := 0 to High(paths[i]) do begin tmpX := pp.X; pp.X := tmpX * matrix[0, 0] + pp.Y * matrix[1, 0] + matrix[2, 0]; pp.Y := tmpX * matrix[0, 1] + pp.Y * matrix[1, 1] + matrix[2, 1]; inc(pp); end; end; end; //------------------------------------------------------------------------------ procedure MatrixApply(const matrix: TMatrixD; img: TImage32; scaleAdjust: Boolean); begin AffineTransformImage(img, matrix, scaleAdjust); end; //------------------------------------------------------------------------------ procedure MatrixApply(const matrix: TMatrixD; img, targetImg: TImage32; scaleAdjust: Boolean); begin AffineTransformImage(img, targetImg, matrix, scaleAdjust); end; //------------------------------------------------------------------------------ procedure MatrixMultiply(var matrix1: TMatrixD; const matrix2: TMatrixD); var i, j: Integer; m: TMatrixD; begin for i := 0 to 2 do for j := 0 to 2 do m[i, j] := (matrix1[i, 0] * matrix2[0, j]) + (matrix1[i, 1] * matrix2[1, j]) + (matrix1[i, 2] * matrix2[2, j]); matrix1 := m; end; //------------------------------------------------------------------------------ procedure MatrixMultiply2(const matrix1: TMatrixD; var matrix2: TMatrixD); var i, j: Integer; m: TMatrixD; begin for i := 0 to 2 do for j := 0 to 2 do m[i, j] := (matrix1[i, 0] * matrix2[0, j]) + (matrix1[i, 1] * matrix2[1, j]) + (matrix1[i, 2] * matrix2[2, j]); matrix2 := m; end; //------------------------------------------------------------------------------ procedure MatrixScale(var matrix: TMatrixD; scaleX, scaleY: double); var m: TMatrixD; begin m := IdentityMatrix; if (scaleX = 0) or (scaleY = 0) then raise Exception(rsInvalidScale); if ValueAlmostOne(scaleX) and ValueAlmostOne(scaleY) then Exit; m[0, 0] := scaleX; m[1, 1] := scaleY; MatrixMultiply(matrix, m); end; //------------------------------------------------------------------------------ procedure MatrixScale(var matrix: TMatrixD; scale: double); begin if (scale = 0) or (scale = 1) then Exit; MatrixScale(matrix, scale, scale); end; //------------------------------------------------------------------------------ procedure MatrixRotate(var matrix: TMatrixD; const center: TPointD; angRad: double); var m: TMatrixD; sinA, cosA: double; begin if not PointsEqual(center, NullPointD) then begin NormalizeAngle(angRad); if angRad = 0 then Exit; {$IFNDEF CLOCKWISE_ROTATION_WITH_NEGATIVE_ANGLES} angRad := -angRad; //negated angle because of inverted Y-axis. {$ENDIF} m := IdentityMatrix; MatrixTranslate(matrix, -center.X, -center.Y); GetSinCos(angRad, sinA, cosA); m := IdentityMatrix; m[0, 0] := cosA; m[1, 0] := sinA; m[0, 1] := -sinA; m[1, 1] := cosA; MatrixMultiply(matrix, m); MatrixTranslate(matrix, center.X, center.Y); end else MatrixRotate(matrix, angRad) end; //------------------------------------------------------------------------------ procedure MatrixRotate(var matrix: TMatrixD; angRad: double); var m: TMatrixD; sinA, cosA: double; begin NormalizeAngle(angRad); if angRad = 0 then Exit; {$IFNDEF CLOCKWISE_ROTATION_WITH_NEGATIVE_ANGLES} angRad := -angRad; //negated angle because of inverted Y-axis. {$ENDIF} m := IdentityMatrix; GetSinCos(angRad, sinA, cosA); m := IdentityMatrix; m[0, 0] := cosA; m[1, 0] := sinA; m[0, 1] := -sinA; m[1, 1] := cosA; MatrixMultiply(matrix, m); end; //------------------------------------------------------------------------------ procedure MatrixTranslate(var matrix: TMatrixD; dx, dy: double); var m: TMatrixD; begin if ValueAlmostZero(dx) and ValueAlmostZero(dy) then Exit; m := IdentityMatrix; m[2, 0] := dx; m[2, 1] := dy; MatrixMultiply(matrix, m); end; //------------------------------------------------------------------------------ procedure ScaleInternal(var matrix: TMatrixD; s: double); var i, j: Integer; begin for i := 0 to 2 do for j := 0 to 2 do matrix[i,j] := matrix[i,j] * s; end; //------------------------------------------------------------------------------ function MatrixInvert(var matrix: TMatrixD): Boolean; var d: double; const tolerance = 1.0E-5; begin d := MatrixDeterminant(matrix); Result := abs(d) > tolerance; if Result then begin matrix := MatrixAdjugate(matrix); ScaleInternal(matrix, 1/d); end; end; //------------------------------------------------------------------------------ procedure MatrixSkew(var matrix: TMatrixD; angleX, angleY: double); var m: TMatrixD; begin if ValueAlmostZero(angleX) and ValueAlmostZero(angleY) then Exit; m := IdentityMatrix; m[1, 0] := tan(angleX); m[0, 1] := tan(angleY); MatrixMultiply(matrix, m); end; //------------------------------------------------------------------------------ procedure MatrixExtractScale(const mat: TMatrixD; out X, Y: double); begin // https://stackoverflow.com/a/32125700/359538 X := Sqrt(Sqr(mat[0,0]) + Sqr(mat[0,1])); //Y := Sqrt(Sqr(mat[1,0]) + Sqr(mat[1,1])); Y := Abs((mat[0,0] * mat[1,1] - mat[1,0] * mat[0,1]) / X); end; //------------------------------------------------------------------------------ procedure MatrixExtractScale(const mat: TMatrixD; out scale: double); var x,y: double; begin MatrixExtractScale(mat, x, y); scale := Average(x,y); end; //------------------------------------------------------------------------------ procedure MatrixExtractTranslation(const mat: TMatrixD; out dx, dy: double); begin dx := mat[2,0]; dy := mat[2,1]; end; //------------------------------------------------------------------------------ procedure MatrixExtractRotation(const mat: TMatrixD; out angle: double); begin angle := ArcTan2(mat[0,1], mat[0,0]); end; //------------------------------------------------------------------------------ function MatrixExtractAll(const mat: TMatrixD; out angle: double; out scale, trans: TPointD): Boolean; var m00, m01, m10, m11: double; begin m00 := mat[0][0]; m10 := mat[1][0]; m01 := mat[0][1]; m11 := mat[1][1]; trans.X := mat[2][0]; trans.Y := mat[2][1]; angle := 0; scale := PointD(1,1); Result := (m00 <> 0) or (m01 <> 0); if not Result then Exit; angle := ArcTan2(m01, m00); // https://stackoverflow.com/a/32125700/359538 scale.X := Sqrt(Sqr(mat[0,0]) + Sqr(mat[0,1])); scale.Y := (m00 * m11 - m10 * m01) / scale.X; end; //------------------------------------------------------------------------------ {$IFDEF USE_DOWNSAMPLER_AUTOMATICALLY} function CanUseBoxDownsampler(const mat: TMatrixD; sx, sy: double): Boolean; begin // If the matrix looks like this after removing the scale, // the box downsampler can be used. (only translation and scaling) // cos(0) -sin(0) tx 1 0 tx // sin(0) cos(0) ty => 0 1 ty // 0 0 1 0 0 1 { Result := (mat[0,0]/sx = 1) and (mat[0,1]/sx = 0) and (mat[1,0]/sy = 0) and (mat[1,1]/sy = 1) and (mat[2,0] = 0) and (mat[2,1] = 0) and (mat[2,2] = 1); } // We can skip the divisions, because m/s is only zero if m is zero // and m/s=1 is the same as m=s Result := (SameValue(mat[0,0], sx)) and (mat[0,1] = 0) and (mat[1,0] = 0) and (SameValue(mat[1,1], sy)) and (mat[2,0] = 0) and (mat[2,1] = 0) and (mat[2,2] = 1); end; {$ENDIF USE_DOWNSAMPLER_AUTOMATICALLY} //------------------------------------------------------------------------------ // Affine Transformation //------------------------------------------------------------------------------ function GetTransformBounds(img: TImage32; const matrix: TMatrixD): TRect; var pts: TPathD; begin pts := Rectangle(img.Bounds); MatrixApply(matrix, pts); Result := GetBounds(pts); end; //------------------------------------------------------------------------------ function AffineTransformImage(img: TImage32; matrix: TMatrixD; scaleAdjust: Boolean): TPoint; begin Result := AffineTransformImage(img, img, matrix, scaleAdjust); end; //------------------------------------------------------------------------------ function AffineTransformImage(img, targetImg: TImage32; matrix: TMatrixD; scaleAdjust: Boolean): TPoint; var i, j: integer; newWidth, newHeight: integer; sx, sy, x,y: double; xLimLo, yLimLo, xLimHi, yLimHi: double; pc: PColor32; tmp: TArrayOfColor32; dstRec: TRect; resampler: TResamplerFunction; {$IFDEF USE_DOWNSAMPLER_AUTOMATICALLY} useBoxDownsampler: Boolean; {$ENDIF} begin Result := NullPoint; if IsIdentityMatrix(matrix) or img.IsEmpty or (targetImg.Resampler = 0) then begin if targetImg <> img then targetImg.Assign(img); Exit; end; resampler := GetResampler(targetImg.Resampler); if not Assigned(resampler) then begin if targetImg <> img then targetImg.Assign(img); Exit; end; //auto-resize the image so it'll fit transformed image dstRec := img.Bounds; MatrixApply(matrix, dstRec); RectWidthHeight(dstRec, newWidth, newHeight); MatrixExtractScale(matrix, sx, sy); {$IFDEF USE_DOWNSAMPLER_AUTOMATICALLY} if (sx < 1.0) and (sy < 1.0) then begin //only use box downsampling when downsizing useBoxDownsampler := CanUseBoxDownsampler(matrix, sx, sy); end else useBoxDownsampler := false; if useBoxDownsampler then begin BoxDownSampling(img, targetImg, sx, sy); Exit; end; {$ENDIF} if scaleAdjust then begin sx := Max(1, sx * 0.5); sy := Max(1, sy * 0.5); end; //auto-translate the image too Result := dstRec.TopLeft; //starting with the result pixel coords, reverse lookup //the fractional coordinates in the untransformed image if not MatrixInvert(matrix) then begin if targetImg <> img then targetImg.Assign(img); Exit; end; NewColor32Array(tmp, newWidth * newHeight, True); pc := @tmp[0]; xLimLo := -0.5/sx; xLimHi := img.Width + 0.5/sx; yLimLo := -0.5/sy; yLimHi := img.Height + 0.5/sy; for i := dstRec.Top to dstRec.Bottom -1 do begin for j := dstRec.Left to dstRec.Right -1 do begin x := j; y := i; MatrixApply(matrix, x, y); if (x <= xLimLo) or (x >= xLimHi) or (y <= yLimLo) or (y >= yLimHi) then pc^ := clNone32 else // nb: -0.5 below is needed to properly center the transformed image // (and this is most obviously needed when there is large scaling) pc^ := resampler(img, x - 0.5, y - 0.5); inc(pc); end; end; targetImg.AssignPixelArray(tmp, newWidth, newHeight); end; //------------------------------------------------------------------------------ // Projective Transformation //------------------------------------------------------------------------------ procedure MatrixMulCoord(const matrix: TMatrixD; var x,y,z: double); {$IFDEF INLINE} inline; {$ENDIF} var xx, yy: double; begin xx := x; yy := y; x := matrix[0,0] *xx + matrix[0,1] *yy + matrix[0,2] *z; y := matrix[1,0] *xx + matrix[1,1] *yy + matrix[1,2] *z; z := matrix[2,0] *xx + matrix[2,1] *yy + matrix[2,2] *z; end; //------------------------------------------------------------------------------ function BasisToPoints(x1, y1, x2, y2, x3, y3, x4, y4: double): TMatrixD; var m2: TMatrixD; z4: double; begin Result := Matrix(x1, x2, x3, y1, y2, y3, 1, 1, 1); m2 := MatrixAdjugate(Result); z4 := 1; MatrixMulCoord(m2, x4, y4, z4); m2 := Matrix(x4, 0, 0, 0, y4, 0, 0, 0, z4); MatrixMultiply(Result, m2); end; //------------------------------------------------------------------------------ procedure GetSrcCoords256(const matrix: TMatrixD; var x, y: integer); {$IFDEF INLINE} inline; {$ENDIF} var xx,yy,zz: double; const Q: integer = MaxInt div 256; begin //returns coords multiplied by 256 in anticipation of the following //GetWeightedPixel function call which in turn expects the lower 8bits //of the integer coord value to represent a fraction. xx := x; yy := y; zz := 1; MatrixMulCoord(matrix, xx, yy, zz); if zz = 0 then begin if xx >= 0 then x := Q else x := -MaxInt; if yy >= 0 then y := Q else y := -MaxInt; end else begin xx := xx/zz; if xx > Q then x := MaxInt else if xx < -Q then x := -MaxInt else x := Round(xx *256); yy := yy/zz; if yy > Q then y := MaxInt else if yy < -Q then y := -MaxInt else y := Round(yy *256); end; end; //------------------------------------------------------------------------------ procedure GetSrcCoords(const matrix: TMatrixD; var x, y: double); {$IFDEF INLINE} inline; {$ENDIF} var zz: double; const Q: integer = MaxInt div 256; begin //returns coords multiplied by 256 in anticipation of the following //GetWeightedPixel function call which in turn expects the lower 8bits //of the integer coord value to represent a fraction. zz := 1; MatrixMulCoord(matrix, x, y, zz); if zz = 0 then begin if x >= 0 then x := Q else x := -MaxDouble; if y >= 0 then y := Q else y := -MaxDouble; end else begin x := x/zz; if x > Q then x := MaxDouble else if x < -Q then x := -MaxDouble; y := y/zz; if y > Q then y := MaxDouble else if y < -Q then y := -MaxDouble end; end; //------------------------------------------------------------------------------ function GetProjectionMatrix(const srcPts, dstPts: TPathD): TMatrixD; var dstMat: TMatrixD; begin if (length(srcPts) <> 4) or (length(dstPts) <> 4) then begin Result := IdentityMatrix; Exit; end; Result := BasisToPoints(srcPts[0].X, srcPts[0].Y, srcPts[1].X, srcPts[1].Y, srcPts[2].X, srcPts[2].Y, srcPts[3].X, srcPts[3].Y); dstMat := BasisToPoints(dstPts[0].X, dstPts[0].Y, dstPts[1].X, dstPts[1].Y, dstPts[2].X, dstPts[2].Y, dstPts[3].X, dstPts[3].Y); MatrixMultiply(Result, MatrixAdjugate(dstMat)); end; //------------------------------------------------------------------------------ function ProjectiveTransform(img: TImage32; const srcPts, dstPts: TPathD; const margins: TRect): Boolean; begin Result := ProjectiveTransform(img, img, srcPts, dstPts, margins); end; //------------------------------------------------------------------------------ function ProjectiveTransform(img, targetImg: TImage32; const srcPts, dstPts: TPathD; const margins: TRect): Boolean; var w,h,i,j: integer; x,y: double; xLimLo, yLimLo, xLimHi, yLimHi: double; rec: TRect; dstPts2: TPathD; mat: TMatrixD; tmp: TArrayOfColor32; pc: PColor32; resampler: TResamplerFunction; begin //https://math.stackexchange.com/a/339033/384709 if targetImg.Resampler = 0 then resampler := nil else resampler := GetResampler(targetImg.Resampler); Result := Assigned(resampler) and not img.IsEmpty and (Length(dstPts) = 4) and IsPathConvex(dstPts); if not Result then begin if targetImg <> img then targetImg.Assign(img); Exit; end; rec := GetBounds(dstPts); dec(rec.Left, margins.Left); dec(rec.Top, margins.Top); inc(rec.Right, margins.Right); inc(rec.Bottom, margins.Bottom); dstPts2 := TranslatePath(dstPts, -rec.Left, -rec.Top); xLimLo := -0.5; xLimHi := img.Width + 0.5; yLimLo := -0.5; yLimHi := img.Height + 0.5; mat := GetProjectionMatrix(srcPts, dstPts2); RectWidthHeight(rec, w, h); NewColor32Array(tmp, w * h, True); pc := @tmp[0]; for i := 0 to h -1 do for j := 0 to w -1 do begin x := j; y := i; GetSrcCoords(mat, x, y); if (x <= xLimLo) or (x >= xLimHi) or (y <= yLimLo) or (y >= yLimHi) then pc^ := clNone32 else pc^ := resampler(img, x -0.5, y -0.5); inc(pc); end; targetImg.BlockNotify; targetImg.AssignPixelArray(tmp, w, h); targetImg.UnblockNotify; end; //------------------------------------------------------------------------------ // Spline transformations //------------------------------------------------------------------------------ function ReColor(color, newColor: TColor32): TColor32; {$IFDEF INLINE} inline; {$ENDIF} begin Result := (color and $FF000000) or newColor; end; //------------------------------------------------------------------------------ function InterpolateSegX(const pt1, pt2: TPointD): TPathD; var i, x1, x2: integer; xo,dydx: double; begin Result := nil; if pt2.X > pt1.X then begin x1 := Ceil(pt1.X); x2 := Ceil(pt2.X); if x1 = x2 then Exit; dydx := (pt2.Y - pt1.Y)/(pt2.X - pt1.X); xo := x1 -pt1.X; NewPointDArray(Result, x2-x1, True); for i:= 0 to x2 - x1 -1 do begin Result[i].X := x1 +i; Result[i].Y := pt1.Y + dydx * (xo +i); end; end else begin x1 := Floor(pt1.X); x2 := Floor(pt2.X); if x1 = x2 then Exit; dydx := (pt2.Y - pt1.Y)/(pt2.X - pt1.X); xo := x1 -pt1.X; NewPointDArray(Result, x1-x2, True); for i:= 0 to x1 - x2 -1 do begin Result[i].X := x1 -i; Result[i].Y := pt1.Y + dydx * (xo -i); end; end; end; //------------------------------------------------------------------------------ function InterpolateSegY(const pt1, pt2: TPointD): TPathD; var i, y1,y2: integer; yo,dxdy: double; begin Result := nil; if pt2.Y > pt1.Y then begin y1 := Ceil(pt1.Y); y2 := Ceil(pt2.Y); if y1 = y2 then Exit; dxdy := (pt2.X - pt1.X)/(pt2.Y - pt1.Y); yo := y1 -pt1.Y; NewPointDArray(Result, y2-y1, True); for i:= 0 to y2 - y1 -1 do begin Result[i].Y := y1 +i; Result[i].X := pt1.X + dxdy * (yo +i); end; end else begin y1 := Floor(pt1.Y); y2 := Floor(pt2.Y); if y1 = y2 then Exit; dxdy := (pt2.X - pt1.X)/(pt2.Y - pt1.Y); yo := y1 -pt1.Y; NewPointDArray(Result, y1-y2, True); for i:= 0 to y1 - y2 -1 do begin Result[i].Y := y1 -i; Result[i].X := pt1.X + dxdy * (yo -i); end; end; end; //------------------------------------------------------------------------------ function InterpolatePathForX(const path: TPathD): TPathD; var i,len: integer; tmp: TPathD; begin Result := nil; len := length(path); if len < 2 then Exit; for i := 1 to len -1 do begin tmp := InterpolateSegX(path[i-1], path[i]); ConcatPaths(Result, tmp); end; end; //------------------------------------------------------------------------------ function InterpolatePathForY(const path: TPathD): TPathD; var i, len: integer; tmp: TPathD; begin Result := nil; len := length(path); if len < 2 then Exit; for i := 1 to len -1 do begin tmp := InterpolateSegY(path[i-1], path[i]); ConcatPaths(Result, tmp); end; end; //------------------------------------------------------------------------------ function SplineVertTransform(img: TImage32; const topSpline: TPathD; splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean; begin Result := SplineVertTransform(img, img, topSpline, splineType, backColor, offset); end; //------------------------------------------------------------------------------ function SplineVertTransform(img, targetImg: TImage32; const topSpline: TPathD; splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean; var i,j, w,h, len: integer; x,y, yy, q: double; yLimLo, yLimHi: double; distances: TArrayOfDouble; pc: PColor32; rec: TRect; tmp: TArrayOfColor32; topPath: TPathD; prevX: double; resampler: TResamplerFunction; backColoring, allowBackColoring: Boolean; begin offset := NullPoint; if targetImg.Resampler = 0 then resampler := nil else resampler := GetResampler(targetImg.Resampler); //convert the top spline control points into a flattened path if splineType = stQuadratic then topPath := FlattenQSpline(topSpline) else topPath := FlattenCSpline(topSpline); rec := GetBounds(topPath); //return false if the spline is invalid or there's no vertical transformation Result := Assigned(resampler) and not IsEmptyRect(rec); if not Result then begin if targetImg <> img then targetImg.Assign(img); Exit; end; offset := rec.TopLeft; topPath := InterpolatePathForX(topPath); len := Length(topPath); inc(rec.Bottom, img.Height); RectWidthHeight(rec, w, h); NewColor32Array(tmp, (w+1) * h, False); prevX := topPath[0].X; allowBackColoring := GetAlpha(backColor) > 2; backColor := backColor and $00FFFFFF; distances := GetCumulativeDistances(topPath); q := img.Width / distances[High(distances)]; yLimLo := -0.5; yLimHi := img.Height + 0.5; for i := 0 to len -1 do begin pc := @tmp[Round(topPath[i].X)-rec.Left]; backColoring := allowBackColoring and (prevX >= topPath[i].X); prevX := topPath[i].X; yy := topPath[i].Y; for j := rec.top to rec.bottom -1 do begin x := Distances[i]*q; y := j - yy; if (y < yLimLo) or (y > yLimHi) then // do nothing ! else if backColoring then pc^ := BlendToAlpha(pc^, ReColor(resampler(img, x -0.5, y -0.5), backColor)) else pc^ := BlendToAlpha(pc^, resampler(img, x -0.5, y -0.5)); inc(pc, w); end; end; // tmp was creates with "(w+1)*h". We take advantage of the // memory manager's inplace shrink. SetLength(tmp, w * h); targetImg.AssignPixelArray(tmp, w, h); end; //------------------------------------------------------------------------------ function SplineHorzTransform(img: TImage32; const leftSpline: TPathD; splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean; begin Result := SplineHorzTransform(img, img, leftSpline, splineType, backColor, offset); end; //------------------------------------------------------------------------------ function SplineHorzTransform(img, targetImg: TImage32; const leftSpline: TPathD; splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean; var i,j, len, w,h: integer; x,y, xx, q, prevY: double; xLimLo, xLimHi: double; leftPath: TPathD; distances: TArrayOfDouble; rec: TRect; pc: PColor32; tmp: TArrayOfColor32; backColoring, allowBackColoring: Boolean; resampler: TResamplerFunction; begin offset := NullPoint; if targetImg.Resampler = 0 then resampler := nil else resampler := GetResampler(targetImg.Resampler); //convert the left spline control points into a flattened path if splineType = stQuadratic then leftPath := FlattenQSpline(leftSpline) else leftPath := FlattenCSpline(leftSpline); rec := GetBounds(leftPath); //return false if the spline is invalid or there's no horizontal transformation Result := Assigned(resampler) and not IsEmptyRect(rec); if not Result then begin if targetImg <> img then targetImg.Assign(img); Exit; end; offset := rec.TopLeft; leftPath := InterpolatePathForY(leftPath); len := Length(leftPath); inc(rec.Right, img.Width); RectWidthHeight(rec, w, h); NewColor32Array(tmp, w * (h+1), False); prevY := leftPath[0].Y; allowBackColoring := GetAlpha(backColor) > 2; backColor := backColor and $00FFFFFF; distances := GetCumulativeDistances(leftPath); q := img.Height / distances[High(distances)];; xLimLo := -0.5; xLimHi := img.Width + 0.5; for i := 0 to len -1 do begin pc := @tmp[Round(leftPath[i].Y - rec.Top) * w]; backColoring := allowBackColoring and (prevY >= leftPath[i].Y); prevY := leftPath[i].Y; xx := leftPath[i].X; y := Distances[i]*q; for j := rec.left to rec.right -1 do begin x := j - xx; if (x < xLimLo) or (x > xLimHi) then // do nothing ! else if backColoring then pc^ := BlendToAlpha(pc^, ReColor(resampler(img, x -0.5, y -0.5), backColor)) else pc^ := BlendToAlpha(pc^, resampler(img, x -0.5, y -0.5)); inc(pc); end; end; // tmp was creates with "w*(h+1)". We take advantage of the // memory manager's inplace shrink. SetLength(tmp, w * h); targetImg.AssignPixelArray(tmp, w, h); end; //------------------------------------------------------------------------------ // Miscellaneous WeightedColor function //------------------------------------------------------------------------------ function LimitByte(val: Cardinal): byte; {$IFDEF INLINE} inline; {$ENDIF} begin if val > 255 then result := 255 else result := val; end; //------------------------------------------------------------------------------ // TWeightedColor //------------------------------------------------------------------------------ procedure TWeightedColor.Reset; {$IFDEF CPUX64} var Zero: Int64; {$ENDIF CPUX64} begin {$IFDEF CPUX64} Zero := 0; fAddCount := Zero; fAlphaTot := Zero; fColorTotR := Zero; fColorTotG := Zero; fColorTotB := Zero; {$ELSE} fAddCount := 0; fAlphaTot := 0; fColorTotR := 0; fColorTotG := 0; fColorTotB := 0; {$ENDIF CPUX64} end; //------------------------------------------------------------------------------ procedure TWeightedColor.Reset(c: TColor32; w: Integer); var a: Cardinal; begin fAddCount := w; a := w * Byte(c shr 24); if a = 0 then begin fAlphaTot := 0; fColorTotB := 0; fColorTotG := 0; fColorTotR := 0; end else begin fAlphaTot := a; fColorTotB := (a * Byte(c)); fColorTotG := (a * Byte(c shr 8)); fColorTotR := (a * Byte(c shr 16)); end; end; //------------------------------------------------------------------------------ procedure TWeightedColor.AddWeight(w: Integer); begin inc(fAddCount, w); end; //------------------------------------------------------------------------------ procedure TWeightedColor.Add(c: TColor32; w: Integer); var a: Int64; begin inc(fAddCount, w); a := Byte(c shr 24); if a <> 0 then begin a := a * Cardinal(w); inc(fAlphaTot, a); inc(fColorTotB, (a * Byte(c))); inc(fColorTotG, (a * Byte(c shr 8))); inc(fColorTotR, (a * Byte(c shr 16))); end; end; //------------------------------------------------------------------------------ procedure TWeightedColor.Add(c: TColor32); // Optimized for w=1 var a: Int64; begin inc(fAddCount); a := Byte(c shr 24); if a = 0 then Exit; inc(fAlphaTot, a); inc(fColorTotB, (a * Byte(c))); inc(fColorTotG, (a * Byte(c shr 8))); inc(fColorTotR, (a * Byte(c shr 16))); end; //------------------------------------------------------------------------------ procedure TWeightedColor.Add(const other: TWeightedColor); begin inc(fAddCount, other.fAddCount); inc(fAlphaTot, other.fAlphaTot); inc(fColorTotR, other.fColorTotR); inc(fColorTotG, other.fColorTotG); inc(fColorTotB, other.fColorTotB); end; //------------------------------------------------------------------------------ procedure TWeightedColor.Subtract(c: TColor32; w: Integer); var a: Int64; begin dec(fAddCount, w); a := w * Byte(c shr 24); if a = 0 then Exit; dec(fAlphaTot, a); dec(fColorTotB, (a * Byte(c))); dec(fColorTotG, (a * Byte(c shr 8))); dec(fColorTotR, (a * Byte(c shr 16))); end; //------------------------------------------------------------------------------ procedure TWeightedColor.Subtract(c: TColor32); // Optimized for w=1 var a: Int64; begin dec(fAddCount); a := Byte(c shr 24); if a = 0 then Exit; dec(fAlphaTot, a); dec(fColorTotB, (a * Byte(c))); dec(fColorTotG, (a * Byte(c shr 8))); dec(fColorTotR, (a * Byte(c shr 16))); end; //------------------------------------------------------------------------------ procedure TWeightedColor.Subtract(const other: TWeightedColor); begin dec(fAddCount, other.fAddCount); dec(fAlphaTot, other.fAlphaTot); dec(fColorTotR, other.fColorTotR); dec(fColorTotG, other.fColorTotG); dec(fColorTotB, other.fColorTotB); end; //------------------------------------------------------------------------------ function TWeightedColor.AddSubtract(addC, subC: TColor32): Boolean; var a: Int64; begin // add+subtract => fAddCount stays the same // skip identical colors Result := False; if addC = subC then Exit; a := Byte(addC shr 24); if a > 0 then begin inc(fAlphaTot, a); inc(fColorTotB, (a * Byte(addC))); inc(fColorTotG, (a * Byte(addC shr 8))); inc(fColorTotR, (a * Byte(addC shr 16))); Result := True; end; a := Byte(subC shr 24); if a > 0 then begin dec(fAlphaTot, a); dec(fColorTotB, (a * Byte(subC))); dec(fColorTotG, (a * Byte(subC shr 8))); dec(fColorTotR, (a * Byte(subC shr 16))); Result := True; end; end; //------------------------------------------------------------------------------ function TWeightedColor.AddNoneSubtract(c: TColor32): Boolean; var a: Int64; begin // add+subtract => fAddCount stays the same a := Byte(c shr 24); if a > 0 then begin dec(fAlphaTot, a); dec(fColorTotB, (a * Byte(c))); dec(fColorTotG, (a * Byte(c shr 8))); dec(fColorTotR, (a * Byte(c shr 16))); Result := True; end else Result := False; end; //------------------------------------------------------------------------------ function TWeightedColor.GetColor: TColor32; var oneDivAlphaTot: double; alpha: Integer; begin result := clNone32; if (fAlphaTot <= 0) or (fAddCount <= 0) then Exit; {$IFDEF CPUX86} if fAlphaTot and $FFFFFFFF80000000 = 0 then // small, so can avoid _lldiv call alpha := (Cardinal(fAlphaTot) + (Cardinal(fAddCount) shr 1)) div Cardinal(fAddCount) else {$ENDIF CPUX86} alpha := (fAlphaTot + (Cardinal(fAddCount) shr 1)) div Cardinal(fAddCount); result := TColor32(Min(255, alpha)) shl 24; // alpha weighting has been applied to color channels, so div by fAlphaTot if fAlphaTot < DivOneByXTableSize then // use precalculated 1/X values oneDivAlphaTot := DivOneByXTable[fAlphaTot] else oneDivAlphaTot := 1/(fAlphaTot); // 1. Skip zero calculations. // 2. LimitByte(Integer): Values can't be less than 0, so don't use ClampByte. // 3. x86: Round expects the value in the st(0)/xmm1 FPU register. // Thus we need to do the calculation and Round call in one expression. // Otherwise the compiler will use a temporary double variable on // the stack that will cause unnecessary store and load operations. if fColorTotB > 0 then result := result or LimitByte(System.Round(fColorTotB * oneDivAlphaTot)); if fColorTotG > 0 then result := result or LimitByte(System.Round(fColorTotG * oneDivAlphaTot)) shl 8; if fColorTotR > 0 then result := result or LimitByte(System.Round(fColorTotR * oneDivAlphaTot)) shl 16; end; //------------------------------------------------------------------------------ // Initialization //------------------------------------------------------------------------------ procedure MakeDivOneByXTable; var i: Integer; begin DivOneByXTable[0] := 0; // NaN for i := 1 to High(DivOneByXTable) do DivOneByXTable[i] := 1/i; end; initialization MakeDivOneByXTable; end.