doublecmd/components/Image32/source/Img32.Transform.pas
2025-04-20 16:26:15 +03:00

1375 lines
42 KiB
ObjectPascal

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.