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-implementing ImageTransformation
's defaults behavior, and rearranging values for reshapePlotRangeCorners
which does the work.
Composition[TranslationTransform[...], ScalingTransform[...], RotationTransform[...] ]
to create a complex transformation from simple ones $\endgroup$