How to translate and scale an image via ImageTransformation

According to the documentation the coordinate system used by ImageTransformation uses $(w,h)=(1,\alpha)$ where $\alpha$ is the aspect ratio. I'll use an image with aspect ratio 1:

img = ExampleData[{"TestImage", "Lena"}];
aspectRatio = Divide @@ ImageDimensions[img]
(* Out: 1 *)

In order to translate the image we can move each pixel 25% downwards and to the left by adding 0.25, since the full width and height is 1.

ImageTransformation[img, # + 0.25 &]

translate

In order to scale the image we can move each pixel by a factor:

ImageTransformation[img, 1.5 # &]

scale

In order to rotate the image we can use RotationTransform. In this case we specify a rotation of 45 degree around the center of the image:

ImageTransformation[img, RotationTransform[45 Degree, {0.5, aspectRatio/2}]

rotate

Like nikie mentions in a comment you can use Composition to put different transformations together. But you can also do it in your head, for example to scale an image, translate it and then rotate it: RotationTransform[...][1.5 # + 0.25] &. You can also use TranslationTransform and ScalingTransform if you want, just keep the plot range in mind so that the size of the translation is somewhere between (0,0) and (1,aspectRatio).

Finally, during some of these operations part of the image disappears out of the frame. You can fix this by manipulating PlotRange. For example here is how you would translate the image and increase the size of PlotRange so the full image can still be seen:

ImageTransformation[img, # + 0.25 &, PlotRange -> {{-0.25, 1}, {-0.25, 1}}]

Translation, scaling, padding, and cropping can all be done via manipulation of ImageTransformation's PlotRange and image size options. However, due to the reverse mapping of coordinates from destination to source image, the manipulations are not (at least to me) intuitive.

These two equations describe the mapping of pixel coordinates from the destination image, through the transformation function, and back to the source image:

destXY / imageWidthHeightDest (plotRangeRightTop-plotRangeLeftBottom) + plotRangeLeftBottom == pixelPosition,

(function[pixelPosition] - dataRangeLeftBottom)/(dataRangeRightTop-dataRangeLeftBottom) imageWidthHeightSrc == srcXY, 

Where :

  • destXY == pixel coordinates in destination image,
  • {plotRangeRightTop,plotRangeLeftBottom} == Transpose[PlotRange] {{left,right},{bottom,top}} -> {{left,bottom},{right,top}} ,
  • imageWidthHeightDest == ImageDimensions[destination image],
  • pixelPosition == value passed to coordinate transform function[],
  • {dataRangeLeftBottom,dataRangeRightTop} == Transpose[DataRange],
  • imageWidthHeightSrc == ImageDimensions[source image],
  • srcXY == pixel coordinates in source image

Derived primarily from the first equation, this code extends ImageTransformation with translationPixels, scaleFactor, and padPixels options.

ClearAll[extendedImageTransformation, reshapePlotRangeCorners];

reshapePlotRangeCorners[{plotRangeLeftBottom_, plotRangeRightTop_}, 
  imageWidthHeightDest_, shiftXY_, {padLeftBottom_, padRightTop_}, 
  scaleFactor_] := {(plotRangeLeftBottom + 
     plotRangeRightTop + ((plotRangeLeftBottom - plotRangeRightTop)*
             (imageWidthHeightDest + 
          2*(padLeftBottom + shiftXY)))/(imageWidthHeightDest*
        scaleFactor))/2, 
   (plotRangeLeftBottom + 
     plotRangeRightTop - ((plotRangeLeftBottom - plotRangeRightTop)*
             (imageWidthHeightDest + 
          2*(padRightTop - shiftXY)))/(imageWidthHeightDest*
        scaleFactor))/2}

extendedImageTransformation::usage = 
  "extendedImageTransformation extends ImageTransformation[] with optional parameters that reshape the transformed image:
  translationPixels \[Rule] {xShift,yShift}
  padPixels \[Rule] {{left,right},{top,bottom}} (negative values crop, like in ImagePad[])
  scaleFactor \[Rule] {horizontalScaling,verticalScaling}

  Defaults: scaleFactor\[Rule]{1,1}, translationPixels\[Rule]{0,0}, padPixels\[Rule]{{0,0},{0,0}}";
Options[extendedImageTransformation] = {
   "scaleFactor" -> {1, 1}
   , "translationPixels" -> {0, 0}
   , "padPixels" -> {{0, 0}, {0, 0}}
   };
extendedImageTransformation[image_, function_, 
  Shortest[sizeIn_: {0, 0}, 1]
  , opts : 
   OptionsPattern[{extendedImageTransformation, 
     ImageTransformation}]] := Module[
  {
   scaleFactor = OptionValue["scaleFactor"] (* scaling (horizontal, vertical) >1 \[Rule] larger *)
   , 
   translationPixels = OptionValue["translationPixels"]  (* positive values shift right and up *)
   , 
   padPixels = OptionValue["padPixels"](* {{left,right},{bottom, top}} negative values crop, like ImagePad[] *)

   , size
   , h, w,
   , dataRangeValue
   , plotRangeValue

   , plotRangeCorners
   , plotRangeCornersReshaped
   , plotRangeReshaped
   , sizeReshaped
   , padPixelsCorners
   , plotRangeLeftBottom, plotRangeRightTop, scaledPlotRangeCorners
   },

  {h, w} = ImageDimensions[image];

  dataRangeValue = 
   OptionValue[DataRange] /. {Automatic -> {{0, 1}, {0, h/w}}, 
     Full -> {{0, w}, {0, h}}} ;

  plotRangeValue = 
   OptionValue[PlotRange] /. {Automatic -> dataRangeValue, 
     Full -> {{0, w}, {0, h}}};

  (* Transformed image size defaults to source image size scaled by ratio of PlotRange to DataRange *) 
  size = sizeIn /. {0, 0} -> 
     EuclideanDistance @@@ plotRangeValue / 
       EuclideanDistance @@@  dataRangeValue ImageDimensions[image];

  (* {{left,right},{bottom,top}} \[Rule] {{left,bottom},{right,top}} *)
  plotRangeCorners = Transpose[plotRangeValue];
  padPixelsCorners = Transpose[padPixels];

  plotRangeCornersReshaped = 
   reshapePlotRangeCorners[plotRangeCorners , size, translationPixels,
     padPixelsCorners, scaleFactor];

  plotRangeReshaped = Transpose[plotRangeCornersReshaped];

  sizeReshaped = size + (Plus @@ padPixelsCorners); (* {w,h}+{{paddLeft+paddRight},{paddBottom+paddTop}} *)

  ImageTransformation[
   image, function, sizeReshaped
   , PlotRange -> plotRangeReshaped
   , FilterRules[Join[{opts}, Options[extendedImageTransformation]], 
    Options[ImageTransformation]]
   ]
  ]
(* Help with mixing of optional arguments and Options from
 https://mathematica.stackexchange.com/questions/1567/how-can-i-create-a-function-with-positional-or-named-optional-arguments
*)

Example usage:

img = ExampleData[{"TestImage", "Lena"}];
destImage = extendedImageTransformation[img
  , # &
  , scaleFactor -> {2, 2}
  , padPixels -> {{-220, 455 - 512}, {-200, -220}}
  , translationPixels -> {0, 8}
  ]

Lenna image scaled up and cropped to just eyes

Notes:

  • This only works with 2D images.
  • If the transformation function is only valid over limited domain, the PlotRange manipulations could cause the function to be passed values outside its domain.
  • Most of the code in extendedImageTransformation is re-implementing ImageTransformation's defaults behavior, and rearranging values for reshapePlotRangeCorners which does the work.