Skip to content

Commit 64f1779

Browse files
committed
feat: implement align
Closes: #424
1 parent 69f1649 commit 64f1779

File tree

1 file changed

+150
-0
lines changed

1 file changed

+150
-0
lines changed

src/align/align.ts

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import {
2+
Point,
3+
Image,
4+
alignMinDifference,
5+
ImageColorModel,
6+
ThresholdAlgorithm,
7+
} from '..';
8+
9+
export type LevelingAlgorithm = 'none' | 'minMax' | 'uniform';
10+
11+
export interface AlignOptions {
12+
/**
13+
* Factor by which to scale down the images for the rough alignment phase.
14+
* @default 4
15+
*/
16+
scalingFactor?: number;
17+
/**
18+
* Kernel size for the blur applied to the images before the rough alignment phase.
19+
* @default 3
20+
*/
21+
blurKernelSize?: number;
22+
/**
23+
* Factor by which to multiply the scaling factor to get the margin for the precise alignment phase.
24+
* @default 1.5
25+
*/
26+
precisionFactor?: number;
27+
/**
28+
* Whether to auto level the images before the precise alignment phase. You can chose between `minMax`
29+
* leveling (span all channels from 0 to max value) or `uniform`(keep the color balance).
30+
* @default false
31+
*/
32+
level?: LevelingAlgorithm;
33+
/**
34+
* Threshold algorithm to use for the alignment masks.
35+
* @default 'otsu'
36+
*/
37+
thresholdAlgoritm?: ThresholdAlgorithm;
38+
}
39+
40+
/**
41+
* Align an enlarged crop on a reference crop by doing the min difference.
42+
* A rough alignment is first done on the images scaled down and a precise alignment is then
43+
* applied on the real size images. Only part of the pixels are used for the comparison.
44+
* The pixel to considered are defined by a mask. The algorithm used to create the mask
45+
* is defined by the `thresholdAlgorithm` option.
46+
* @param source - The source image. Must be smaller than the destination image.
47+
* @param destination - The destination image.
48+
* @param options - Aligning options.
49+
* @returns The coordinates of the reference relative to the top-left corner
50+
* of the destination image for an optimal alignment.
51+
*/
52+
export function align(
53+
source: Image,
54+
destination: Image,
55+
options: AlignOptions = {},
56+
): Point {
57+
const {
58+
scalingFactor = 4,
59+
precisionFactor = 1.5,
60+
thresholdAlgoritm = 'otsu',
61+
} = options;
62+
63+
// console.log({ level: options.level });
64+
65+
// rough alignment
66+
const small = prepareForAlign(destination, options);
67+
const smallRef = prepareForAlign(source, options);
68+
69+
const smallMask = getAlignMask(smallRef, thresholdAlgoritm);
70+
71+
const roughAlign = alignMinDifference(smallRef, small, {
72+
mask: smallMask,
73+
startStep: 1,
74+
});
75+
76+
// precise alignment
77+
78+
// number of pixels to add around the rough roi crop for the precise alignment
79+
const margin = scalingFactor * precisionFactor;
80+
const originColumn = Math.max(0, roughAlign.column * scalingFactor - margin);
81+
const originRow = Math.max(0, roughAlign.row * scalingFactor - margin);
82+
83+
const roughCrop = destination.crop({
84+
origin: {
85+
column: originColumn,
86+
row: originRow,
87+
},
88+
width: Math.min(
89+
destination.width - originColumn,
90+
source.width + 2 * margin,
91+
),
92+
height: Math.min(
93+
destination.height - originRow,
94+
source.height + 2 * margin,
95+
),
96+
});
97+
98+
const preciseCrop = prepareForAlign(roughCrop, {
99+
...options,
100+
scalingFactor: 1,
101+
});
102+
const preciseRef = prepareForAlign(source, {
103+
...options,
104+
scalingFactor: 1,
105+
});
106+
const refMask = getAlignMask(preciseRef, thresholdAlgoritm);
107+
108+
const preciseAlign = alignMinDifference(preciseRef, preciseCrop, {
109+
startStep: 1,
110+
mask: refMask,
111+
});
112+
113+
return {
114+
column: originColumn + preciseAlign.column,
115+
row: originRow + preciseAlign.row,
116+
};
117+
}
118+
119+
/**
120+
* Prepare an image to align it with another image.
121+
* @param image - Crop to align.
122+
* @param options - Prepare for align options.
123+
* @returns The prepared image
124+
*/
125+
function prepareForAlign(image: Image, options: AlignOptions = {}) {
126+
const { scalingFactor = 4, blurKernelSize = 3, level = 'minMax' } = options;
127+
128+
const blurred = image.blur({ width: blurKernelSize, height: blurKernelSize });
129+
if (level === 'minMax') {
130+
blurred.increaseContrast({ out: blurred });
131+
} else if (level === 'uniform') {
132+
blurred.increaseContrast({ uniform: true, out: blurred });
133+
}
134+
const scaled = blurred.resize({
135+
xFactor: 1 / scalingFactor,
136+
yFactor: 1 / scalingFactor,
137+
});
138+
return scaled;
139+
}
140+
141+
function getAlignMask(image: Image, algorithm: ThresholdAlgorithm) {
142+
if (image.colorModel !== ImageColorModel.GREY) {
143+
image = image.grey();
144+
}
145+
const mask = image
146+
.threshold({ algorithm })
147+
.invert()
148+
.dilate({ iterations: 2 });
149+
return mask;
150+
}

0 commit comments

Comments
 (0)