|
| 1 | +## Synopsis |
| 2 | + |
| 3 | +Image transformations are fundamental operations in computer graphics and image processing that allow you to manipulate the position, size, shape, and perspective of images. This tutorial covers both affine and projective transformations, providing practical examples and clear explanations of how each parameter affects your image. |
| 4 | + |
| 5 | +; |
| 6 | + |
| 7 | +## Understanding Transformation Types |
| 8 | + |
| 9 | +In this tutorial, we distinguish between two primary types of transformations: |
| 10 | + |
| 11 | +### Affine Transformations |
| 12 | + |
| 13 | +- **Preserve**: Collinearity and ratios of distances |
| 14 | +- **Properties**: Parallel lines remain parallel, straight lines remain straight |
| 15 | +- **Use cases**: Scaling, rotation, translation, shearing |
| 16 | +- **Matrix size**: 2×3 (the bottom row [0, 0, 1] is implied) |
| 17 | + |
| 18 | +### Projective Transformations |
| 19 | + |
| 20 | +- **Preserve**: Only collinearity (straight lines remain straight) |
| 21 | +- **Properties**: Parallel lines may converge, creates perspective effects |
| 22 | +- **Use cases**: Perspective correction, 3D projections, keystone correction |
| 23 | +- **Matrix size**: 3×3 (full matrix required) |
| 24 | +- **The key difference**: affine transformations might stretch, rotate, or shift a rectangle, but parallel lines remain parallel. Projective transformations can make a rectangle appear tilted or receding into the distance, with parallel lines converging to vanishing points. |
| 25 | + |
| 26 | +## The Transformation Matrix |
| 27 | + |
| 28 | +We use this 3×3 matrix throughout the tutorial: |
| 29 | + |
| 30 | +$$ |
| 31 | +\begin{bmatrix} |
| 32 | +a & b & c\\ |
| 33 | +d & e & f \\ |
| 34 | +g & h & i |
| 35 | +\end{bmatrix} |
| 36 | +$$ |
| 37 | + |
| 38 | +Each parameter controls specific aspects of the transformation: |
| 39 | + |
| 40 | +`a`, `e`: Scaling (horizontal and vertical) |
| 41 | +`b`, `d`: Shearing and rotation |
| 42 | +`c`, `f`: Translation (horizontal and vertical) |
| 43 | +`g`, `h`: Perspective distortion |
| 44 | +`i`: Normalization factor (usually 1) |
| 45 | + |
| 46 | +## Getting Started |
| 47 | + |
| 48 | +First, let's load an image: |
| 49 | + |
| 50 | +```ts |
| 51 | +import { readSync } from 'image-processing-library'; |
| 52 | + |
| 53 | +const image = readSync('/path/to/image.png'); |
| 54 | +``` |
| 55 | + |
| 56 | +## Affine Transformations |
| 57 | + |
| 58 | +1. Scaling |
| 59 | + Scaling changes the size of your image. Parameters a and e control horizontal and vertical scaling respectively. |
| 60 | + |
| 61 | +Uniform Scaling (Maintaining Aspect Ratio) |
| 62 | + |
| 63 | +```ts |
| 64 | +// Scale image by factor of 2 (double the size) |
| 65 | +const transformationMatrix = [ |
| 66 | + [2, 0, 0], // a=2 (horizontal scale), b=0, c=0 |
| 67 | + [0, 2, 0], // d=0, e=2 (vertical scale), f=0 |
| 68 | +]; |
| 69 | + |
| 70 | +const scaledImage = image.transform(transformationMatrix); |
| 71 | +``` |
| 72 | + |
| 73 | +### Non-uniform Scaling |
| 74 | + |
| 75 | +```ts |
| 76 | +// Stretch horizontally by 3x, compress vertically by 0.5x |
| 77 | +const transformationMatrix = [ |
| 78 | + [3, 0, 0], // Horizontal stretch |
| 79 | + [0, 0.5, 0], // Vertical compression |
| 80 | +]; |
| 81 | + |
| 82 | +const stretchedImage = image.transform(transformationMatrix); |
| 83 | +``` |
| 84 | + |
| 85 | +#### Common Scaling Examples |
| 86 | + |
| 87 | +```ts |
| 88 | +// Shrink to half size |
| 89 | +const shrinkMatrix = [ |
| 90 | + [0.5, 0, 0], |
| 91 | + [0, 0.5, 0], |
| 92 | +]; |
| 93 | + |
| 94 | +// Mirror horizontally (flip left-right) |
| 95 | +const mirrorMatrix = [ |
| 96 | + [-1, 0, 0], |
| 97 | + [0, 1, 0], |
| 98 | +]; |
| 99 | + |
| 100 | +// Mirror vertically (flip up-down) |
| 101 | +const flipMatrix = [ |
| 102 | + [1, 0, 0], |
| 103 | + [0, -1, 0], |
| 104 | +]; |
| 105 | +``` |
| 106 | + |
| 107 | +### Translation |
| 108 | + |
| 109 | +Translation moves your image to a different position. Parameters `c` and `f` control horizontal and vertical movement. |
| 110 | + |
| 111 | +```ts |
| 112 | +// Move image 50 pixels right and 30 pixels down |
| 113 | +const translationMatrix = [ |
| 114 | + [1, 0, 50], // c=50 (move right) |
| 115 | + [0, 1, 30], // f=30 (move down) |
| 116 | +]; |
| 117 | + |
| 118 | +const translatedImage = image.transform(translationMatrix); |
| 119 | + |
| 120 | +// Move image 100 pixels left and 50 pixels up |
| 121 | +const moveMatrix = [ |
| 122 | + [1, 0, -100], // Negative values move left |
| 123 | + [0, 1, -50], // Negative values move up |
| 124 | +]; |
| 125 | +``` |
| 126 | + |
| 127 | +### Rotation |
| 128 | + |
| 129 | +Rotation transforms your image around a point (typically the origin). It uses a combination of parameters a, b, d, and e. |
| 130 | + |
| 131 | +For rotation by angle θ (in radians): |
| 132 | + |
| 133 | +`a` = cos(θ) |
| 134 | +`b` = -sin(θ) |
| 135 | +`d` = sin(θ) |
| 136 | +`e` = cos(θ) |
| 137 | + |
| 138 | +```ts |
| 139 | +// Rotate 45 degrees clockwise |
| 140 | +const angle = Math.PI / 4; // 45 degrees in radians |
| 141 | +const rotationMatrix = [ |
| 142 | + [Math.cos(angle), -Math.sin(angle), 0], |
| 143 | + [Math.sin(angle), Math.cos(angle), 0], |
| 144 | +]; |
| 145 | + |
| 146 | +const rotatedImage = image.transform(rotationMatrix); |
| 147 | + |
| 148 | +// Rotate 90 degrees counter-clockwise |
| 149 | +const rotation90Matrix = [ |
| 150 | + [0, 1, 0], // cos(90°)=0, -sin(90°)=-(-1)=1 |
| 151 | + [-1, 0, 0], // sin(90°)=1, cos(90°)=0 |
| 152 | +]; |
| 153 | +``` |
| 154 | + |
| 155 | +### Rotation Around Image Center |
| 156 | + |
| 157 | +To rotate around the image center instead of the origin, combine translation with rotation: |
| 158 | + |
| 159 | +```ts |
| 160 | +function rotateAroundCenter(image, angle) { |
| 161 | + const centerX = image.width / 2; |
| 162 | + const centerY = image.height / 2; |
| 163 | + |
| 164 | + const cos = Math.cos(angle); |
| 165 | + const sin = Math.sin(angle); |
| 166 | + |
| 167 | + // Translate to origin, rotate, translate back |
| 168 | + const matrix = [ |
| 169 | + [cos, -sin, centerX * (1 - cos) + centerY * sin], |
| 170 | + [sin, cos, centerY * (1 - cos) - centerX * sin], |
| 171 | + ]; |
| 172 | + |
| 173 | + return image.transform(matrix); |
| 174 | +} |
| 175 | + |
| 176 | +const centeredRotation = rotateAroundCenter(image, Math.PI / 6); // 30 degrees |
| 177 | +``` |
| 178 | + |
| 179 | +### Shearing |
| 180 | + |
| 181 | +Shearing skews the image, making rectangles appear as parallelograms. Parameters b and d control shearing. |
| 182 | + |
| 183 | +```ts |
| 184 | +// Horizontal shear - lean the image to the right |
| 185 | +const horizontalShearMatrix = [ |
| 186 | + [1, 0.5, 0], // b=0.5 creates horizontal shear |
| 187 | + [0, 1, 0], |
| 188 | +]; |
| 189 | + |
| 190 | +// Vertical shear - lean the image upward |
| 191 | +const verticalShearMatrix = [ |
| 192 | + [1, 0, 0], |
| 193 | + [0.3, 1, 0], // d=0.3 creates vertical shear |
| 194 | +]; |
| 195 | + |
| 196 | +// Combined shearing |
| 197 | +const combinedShearMatrix = [ |
| 198 | + [1, 0.5, 0], // Horizontal shear |
| 199 | + [0.3, 1, 0], // Vertical shear |
| 200 | +]; |
| 201 | +``` |
| 202 | + |
| 203 | +### Complex Affine Transformations |
| 204 | + |
| 205 | +You can combine multiple transformations by multiplying matrices or applying them sequentially: |
| 206 | + |
| 207 | +```ts |
| 208 | +// Scale, rotate, and translate in one transformation |
| 209 | +const angle = Math.PI / 4; |
| 210 | +const scale = 1.5; |
| 211 | +const translateX = 100; |
| 212 | +const translateY = 50; |
| 213 | + |
| 214 | +const complexMatrix = [ |
| 215 | + [scale * Math.cos(angle), -scale * Math.sin(angle), translateX], |
| 216 | + [scale * Math.sin(angle), scale * Math.cos(angle), translateY], |
| 217 | +]; |
| 218 | + |
| 219 | +const complexTransform = image.transform(complexMatrix); |
| 220 | +``` |
| 221 | + |
| 222 | +## Projective Transformations |
| 223 | + |
| 224 | +Projective transformations use the full 3×3 matrix, including the bottom row parameters `g`, `h`, and `i`. These create perspective effects and can map rectangular images onto quadrilaterals. |
| 225 | + |
| 226 | +### Understanding Perspective Parameters |
| 227 | + |
| 228 | +`g`, `h`: Control perspective distortion |
| 229 | +`i`: Normalization factor (typically 1) |
| 230 | + |
| 231 | +```ts |
| 232 | +// Simple perspective transformation |
| 233 | +const perspectiveMatrix = [ |
| 234 | + [1, 0, 0], // Standard scaling and translation |
| 235 | + [0, 1, 0], |
| 236 | + [0.001, 0, 1], // g=0.001 creates horizontal perspective |
| 237 | +]; |
| 238 | + |
| 239 | +const perspectiveImage = image.transform(perspectiveMatrix); |
| 240 | +``` |
| 241 | + |
| 242 | +### Four-Point Mapping |
| 243 | + |
| 244 | +The most common use of projective transformation is mapping an image to fit within four corner points: |
| 245 | + |
| 246 | +```ts |
| 247 | +// Define source corners (original image corners) |
| 248 | +const sourcePoints = [ |
| 249 | + [0, 0], // Top-left |
| 250 | + [image.width, 0], // Top-right |
| 251 | + [image.width, image.height], // Bottom-right |
| 252 | + [0, image.height], // Bottom-left |
| 253 | +]; |
| 254 | + |
| 255 | +// Define destination corners (where you want them to appear) |
| 256 | +const destPoints = [ |
| 257 | + [50, 100], // Top-left moved |
| 258 | + [300, 80], // Top-right |
| 259 | + [320, 250], // Bottom-right |
| 260 | + [30, 280], // Bottom-left |
| 261 | +]; |
| 262 | + |
| 263 | +// Calculate transformation matrix (implementation depends on library) |
| 264 | +const projectionMatrix = calculateProjectionMatrix(sourcePoints, destPoints); |
| 265 | +const projectedImage = image.transform(projectionMatrix); |
| 266 | +``` |
| 267 | + |
| 268 | +### Keystone Correction |
| 269 | + |
| 270 | +Correcting perspective distortion (like photographing a screen at an angle): |
| 271 | + |
| 272 | +```ts |
| 273 | +// Correct keystone effect - make trapezoid into rectangle |
| 274 | +const keystoneMatrix = [ |
| 275 | + [1.2, 0.1, -50], |
| 276 | + [0.05, 1.1, -20], |
| 277 | + [0.0002, 0.0001, 1], |
| 278 | +]; |
| 279 | + |
| 280 | +const correctedImage = image.transform(keystoneMatrix); |
| 281 | +``` |
| 282 | + |
| 283 | +## Practical Examples and Use Cases |
| 284 | + |
| 285 | +### Creating Thumbnails with Proper Aspect Ratio |
| 286 | + |
| 287 | +```ts |
| 288 | +function createThumbnail(image, maxWidth, maxHeight) { |
| 289 | + const scaleX = maxWidth / image.width; |
| 290 | + const scaleY = maxHeight / image.height; |
| 291 | + const scale = Math.min(scaleX, scaleY); // Maintain aspect ratio |
| 292 | + |
| 293 | + const thumbnailMatrix = [ |
| 294 | + [scale, 0, 0], |
| 295 | + [0, scale, 0], |
| 296 | + ]; |
| 297 | + |
| 298 | + return image.transform(thumbnailMatrix); |
| 299 | +} |
| 300 | +``` |
| 301 | + |
| 302 | +### Photo Straightening |
| 303 | + |
| 304 | +```ts |
| 305 | +function straightenPhoto(image, angleDegrees) { |
| 306 | +const angle = (angleDegrees \* Math.PI) / 180; |
| 307 | +const centerX = image.width / 2; |
| 308 | +const centerY = image.height / 2; |
| 309 | + |
| 310 | +const cos = Math.cos(-angle); // Negative for correction |
| 311 | +const sin = Math.sin(-angle); |
| 312 | + |
| 313 | +const matrix = [ |
| 314 | +[cos, -sin, centerX * (1 - cos) + centerY * sin], |
| 315 | +[sin, cos, centerY * (1 - cos) - centerX * sin], |
| 316 | +]; |
| 317 | + |
| 318 | +return image.transform(matrix); |
| 319 | +} |
| 320 | +``` |
| 321 | + |
| 322 | +### Document Scanning Perspective Correction |
| 323 | + |
| 324 | +```ts |
| 325 | +function correctDocumentPerspective(image, corners) { |
| 326 | + // corners should be [topLeft, topRight, bottomRight, bottomLeft] |
| 327 | + const [tl, tr, br, bl] = corners; |
| 328 | + |
| 329 | + // Calculate document dimensions |
| 330 | + const width = Math.max(distance(tl, tr), distance(bl, br)); |
| 331 | + const height = Math.max(distance(tl, bl), distance(tr, br)); |
| 332 | + |
| 333 | + // Target rectangle corners |
| 334 | + const targetCorners = [ |
| 335 | + [0, 0], |
| 336 | + [width, 0], |
| 337 | + [width, height], |
| 338 | + [0, height], |
| 339 | + ]; |
| 340 | + |
| 341 | + const matrix = calculateProjectionMatrix(corners, targetCorners); |
| 342 | + return image.transform(matrix); |
| 343 | +} |
| 344 | + |
| 345 | +function distance(p1, p2) { |
| 346 | + return Math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2); |
| 347 | +} |
| 348 | +``` |
0 commit comments