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 &]
In order to scale the image we can move each pixel by a factor:
ImageTransformation[img, 1.5 # &]
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}]
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}
]
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-implementingImageTransformation
's defaults behavior, and rearranging values forreshapePlotRangeCorners
which does the work.