unit Img32.Transform;

(*******************************************************************************
* Author    :  Angus Johnson                                                   *
* Version   :  4.6                                                             *
* Date      :  27 November 2024                                                *
* Website   :  http://www.angusj.com                                           *
* Copyright :  Angus Johnson 2019-2024                                         *
* 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.
