Skip to content

Commit ed97aef

Browse files
committed
wip: start function to test aligning stacks
1 parent 14029d1 commit ed97aef

File tree

6 files changed

+182
-18
lines changed

6 files changed

+182
-18
lines changed

scripts/alignStack/testAlignStack.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { readdirSync } from 'node:fs';
2+
import {
3+
alignStack,
4+
createStackFromPaths,
5+
cropCommonArea,
6+
} from '../../src/stack/index';
7+
import { findCommonArea } from '../../src/stack/align/utils/findCommonArea';
8+
import { crop, writeSync } from '../../src';
9+
10+
const folder = `${__dirname}/BloodMoon`;
11+
let imageNames = readdirSync(folder);
12+
13+
imageNames = imageNames.slice(0, 2);
14+
15+
const paths = imageNames.map((name) => `${folder}/${name}`);
16+
17+
console.log(paths);
18+
19+
console.log('Number of images in stack:', paths.length);
20+
21+
const stack = createStackFromPaths(paths);
22+
23+
console.log(
24+
`Images dimensions: ${stack.getImage(0).width}x${stack.getImage(0).height}`,
25+
);
26+
27+
console.log('Compute absolute translations');
28+
const translations = alignStack(stack, {
29+
minNbPixels: 1000,
30+
debug: true,
31+
scalingFactor: 100,
32+
});
33+
34+
console.log('Cropping all images');
35+
const crops = cropCommonArea(stack, translations.absolute);
36+
37+
console.log('Writing all images');
38+
for (let i = 0; i < crops.size; i++) {
39+
const image = crops.getImage(i);
40+
writeSync(`${__dirname}/result/${imageNames[i]}`, image);
41+
}

src/stack/align/__tests__/alignDifferentSize.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ test('different image sizes, no rounding problem', () => {
5959
maxNbOperations: 1e8,
6060
xFactor: 0.5,
6161
yFactor: 0.5,
62+
debug: true,
6263
});
6364

6465
const overlap = overlapImages(source, destination, { origin: result });

src/stack/align/alignDifferentSize.ts

Lines changed: 115 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
1-
import { Image, Point, ThresholdAlgorithm } from '../..';
1+
import {
2+
Image,
3+
Point,
4+
ThresholdAlgorithm,
5+
overlapImages,
6+
writeSync,
7+
} from '../..';
28
import {
39
LevelingAlgorithm,
410
getAlignMask,
511
prepareForAlign,
612
} from '../../align/align';
13+
import { max } from '../../operations/greyAlgorithms';
714

815
import {
9-
ComputeNbOperationsOptions,
16+
ComputeXYMarginsOptions,
1017
computeNbOperations,
1118
computeXYMargins,
1219
} from './utils/computeNbOperations';
1320
import { findOverlap } from './utils/findOverlap';
1421
import { getMinDiffTranslation } from './utils/getMinDiffTranslation';
1522

16-
export interface AlignDifferentSizeOptions extends ComputeNbOperationsOptions {
23+
export interface AlignDifferentSizeOptions extends ComputeXYMarginsOptions {
1724
/**
1825
* Maximal number of operations that the algorithm can perform.
1926
* This number is used to rescale the images if they are too big so that
2027
* the algorithm takes roughly always the same time to compute.
28+
* You can use scalingFactor instead to specify the scaling factor to apply,
29+
* in that case, maxNbOperations should be undefined.
2130
* @default 1e8
2231
*/
2332
maxNbOperations?: number;
33+
/**
34+
* Scaling factor to apply to the images before the rough alignment phase.
35+
* Can only be used if maxnbOperations is undefined.
36+
* @default undefined
37+
*/
38+
scalingFactor?: number;
2439
/**
2540
* Threshold algorithm to use for the alignment masks.
2641
* @default 'otsu'
@@ -47,10 +62,20 @@ export interface AlignDifferentSizeOptions extends ComputeNbOperationsOptions {
4762
* @default 0.1
4863
*/
4964
minFractionPixels?: number;
65+
/**
66+
* Minimal number of overlapping pixels to apply the algorithm.
67+
* @default undefined
68+
*/
69+
minNbPixels?: number;
70+
/**
71+
* Display debug information?
72+
* @default false
73+
*/
74+
debug?: boolean;
5075
}
5176

5277
/**
53-
* Align two different size images by finding the position wchich minimises the difference.
78+
* Align two different size images by finding the position which minimises the difference.
5479
* @param source - Source image.
5580
* @param destination - Destination image.
5681
* @param options - Align different size options.
@@ -64,34 +89,82 @@ export function alignDifferentSize(
6489
const {
6590
xFactor = 0.5,
6691
yFactor = 0.5,
67-
maxNbOperations = 1e8,
6892
precisionFactor = 1.5,
6993
thresholdAlgoritm = 'otsu',
7094
level = 'minMax',
7195
blurKernelSize,
7296
minFractionPixels,
97+
debug = false,
7398
} = options;
7499

75-
const margins = computeXYMargins(source, destination, { xFactor, yFactor });
100+
let maxNbOperations;
101+
let scalingFactor;
102+
if (
103+
options.maxNbOperations === undefined &&
104+
options.scalingFactor === undefined
105+
) {
106+
maxNbOperations = 1e8;
107+
} else if (
108+
options.maxNbOperations !== undefined &&
109+
options.scalingFactor === undefined
110+
) {
111+
maxNbOperations = options.maxNbOperations;
112+
} else if (
113+
options.maxNbOperations === undefined &&
114+
options.scalingFactor !== undefined
115+
) {
116+
scalingFactor = options.scalingFactor;
117+
} else {
118+
throw new Error('You cannot define both maxNbOperations and scalingFactor');
119+
}
76120

77-
const nbOperations = computeNbOperations(source, destination, margins);
121+
const margins = computeXYMargins(source, destination, { xFactor, yFactor });
78122

79-
let scalingFactor = 1;
80-
if (nbOperations > maxNbOperations) {
81-
scalingFactor = Math.sqrt(nbOperations / maxNbOperations);
123+
if (maxNbOperations !== undefined) {
124+
const initialSrcMask = getAlignMask(source, thresholdAlgoritm);
125+
const initialDstMask = getAlignMask(destination, thresholdAlgoritm);
126+
const nbOperations = computeNbOperations(source, destination, {
127+
sourceMask: initialSrcMask,
128+
destinationMask: initialDstMask,
129+
margins,
130+
});
131+
if (debug) {
132+
console.log({ nbOperations });
133+
}
134+
scalingFactor = 1;
135+
if (nbOperations > maxNbOperations) {
136+
scalingFactor = Math.sqrt(nbOperations / maxNbOperations);
137+
}
138+
}
139+
if (debug) {
140+
console.log({ scalingFactor });
82141
}
142+
83143
// Rough alignment
84144
const smallSource = prepareForAlign(source, {
85145
scalingFactor,
86146
level,
87147
blurKernelSize,
88148
});
89-
const smallMask = getAlignMask(smallSource, thresholdAlgoritm);
149+
const smallSrcMask = getAlignMask(smallSource, thresholdAlgoritm);
90150
const smallDestination = prepareForAlign(destination, {
91151
scalingFactor,
92152
level,
93153
blurKernelSize,
94154
});
155+
const smallDstMask = getAlignMask(smallDestination, thresholdAlgoritm);
156+
157+
const smallNbPixels =
158+
(smallSrcMask.getNbNonZeroPixels() + smallDstMask.getNbNonZeroPixels()) / 2;
159+
160+
if (debug) {
161+
console.log({ smallNbPixels });
162+
writeSync(`${__dirname}/smallSource.png`, smallSource);
163+
writeSync(`${__dirname}/smallDestination.png`, smallDestination);
164+
writeSync(`${__dirname}/smallSrcMask.png`, smallSrcMask);
165+
writeSync(`${__dirname}/smallDstMask.png`, smallDstMask);
166+
}
167+
95168
const smallMargins = computeXYMargins(smallSource, smallDestination, {
96169
xFactor,
97170
yFactor,
@@ -101,14 +174,26 @@ export function alignDifferentSize(
101174
smallSource,
102175
smallDestination,
103176
{
104-
sourceMask: smallMask,
177+
sourceMask: smallSrcMask,
178+
destinationMask: smallDstMask,
105179
leftRightMargin: smallMargins.xMargin,
106180
topBottomMargin: smallMargins.yMargin,
107-
minFractionPixels,
181+
minNbPixels: smallNbPixels,
108182
},
109183
);
110184

185+
if (debug) {
186+
console.log({ roughTranslation });
187+
const overlap = overlapImages(smallSource, smallDestination, {
188+
origin: roughTranslation,
189+
});
190+
writeSync(`${__dirname}/roughOverlap.png`, overlap);
191+
}
192+
111193
// Find overlapping surface and source and destination origins
194+
if (debug) {
195+
console.log('Find overlap');
196+
}
112197
const scaledTranslation = {
113198
column: Math.round(roughTranslation.column * scalingFactor),
114199
row: Math.round(roughTranslation.row * scalingFactor),
@@ -142,23 +227,38 @@ export function alignDifferentSize(
142227
blurKernelSize,
143228
scalingFactor: 1,
144229
});
145-
const mask = getAlignMask(preciseSource, thresholdAlgoritm);
230+
const srcMask = getAlignMask(preciseSource, thresholdAlgoritm);
146231
const preciseDestination = prepareForAlign(destinationCrop, {
147232
level,
148233
blurKernelSize,
149234
scalingFactor: 1,
150235
});
236+
const dstMask = getAlignMask(preciseDestination, thresholdAlgoritm);
237+
if (debug) {
238+
writeSync(`${__dirname}/preciseSource.png`, preciseSource);
239+
writeSync(`${__dirname}/preciseDestination.png`, preciseDestination);
240+
writeSync(`${__dirname}/srcMask.png`, srcMask);
241+
writeSync(`${__dirname}/dstMask.png`, dstMask);
242+
}
243+
244+
const nbPixels =
245+
(srcMask.getNbNonZeroPixels() + dstMask.getNbNonZeroPixels()) / 2;
151246

152247
const preciseMargins = Math.round(precisionFactor * scalingFactor);
153248

249+
if (debug) {
250+
console.log('Compute precise translation');
251+
}
154252
const preciseTranslation = getMinDiffTranslation(
155253
preciseSource,
156254
preciseDestination,
157255
{
158256
leftRightMargin: preciseMargins,
159257
topBottomMargin: preciseMargins,
160258
minFractionPixels,
161-
sourceMask: mask,
259+
sourceMask: srcMask,
260+
destinationMask: dstMask,
261+
minNbPixels: nbPixels,
162262
},
163263
);
164264

src/stack/align/alignStack.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Stack } from '../../Stack';
22
import { Point, sum } from '../../utils/geometry/points';
33

4-
import { alignDifferentSize } from './alignDifferentSize';
4+
import {
5+
AlignDifferentSizeOptions,
6+
alignDifferentSize,
7+
} from './alignDifferentSize';
58

69
export interface Translations {
710
absolute: Point[];
@@ -15,15 +18,21 @@ export interface Translations {
1518
* Internally, the images are aligned between two consecutive images
1619
* (we imagine that there is less variation in consecutive images).
1720
* @param stack - Stack to align.
21+
* @param options - Options to align different size images.
1822
* @returns The relative and absolute translations.
1923
*/
20-
export function alignStack(stack: Stack): Translations {
24+
export function alignStack(
25+
stack: Stack,
26+
options: AlignDifferentSizeOptions = {},
27+
): Translations {
2128
const relativeTranslations: Point[] = [{ column: 0, row: 0 }];
2229
const absoluteTranslations: Point[] = [{ column: 0, row: 0 }];
2330
for (let i = 1; i < stack.size; i++) {
31+
console.log(`Aligning image ${i}`);
2432
const currentRelativeTranslation = alignDifferentSize(
2533
stack.getImage(i),
2634
stack.getImage(i - 1),
35+
options,
2736
);
2837
relativeTranslations.push(currentRelativeTranslation);
2938
absoluteTranslations.push(

src/stack/align/utils/computeNbOperations.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ export interface ComputeNbOperationsOptions {
8585
* @default new Mask(source.width, source.height).fill(1)
8686
*/
8787
sourceMask?: Mask;
88+
/**
89+
* The mask of the destination image.
90+
* @default new Mask(destination.width, destination.height).fill(1)
91+
*/
92+
destinationMask?: Mask;
8893
/**
8994
* The margins around the destination image in which the source can be translated.
9095
* @default { xMargin: 0, yMargin: 0 }
@@ -106,13 +111,17 @@ export function computeNbOperations(
106111
): number {
107112
const {
108113
sourceMask = new Mask(source.width, source.height).fill(1),
114+
destinationMask = new Mask(destination.width, destination.height).fill(1),
109115
margins = defaultMargins,
110116
} = options;
111117
const nbTranslations = computeNbTranslations(source, destination, margins);
112118
const minHeight = Math.min(source.height, destination.height);
113119
const minWidth = Math.min(source.width, destination.width);
114120
// take mask into account
115-
const fractionWhitePixels = sourceMask.getNbNonZeroPixels() / source.size;
121+
const fractionWhitePixels =
122+
(sourceMask.getNbNonZeroPixels() / source.size +
123+
destinationMask.getNbNonZeroPixels() / destination.size) /
124+
2;
116125
console.log({ fractionWhitePixels });
117126

118127
return nbTranslations * minHeight * minWidth * fractionWhitePixels;

src/stack/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@ export * from './compute/maxImage';
33
export * from './compute/meanImage';
44
export * from './compute/medianImage';
55
export * from './load/decodeStack';
6+
export * from './load/createStackFromPaths';
67
export * from './compute/minImage';
78
export * from './compute/sum';
9+
export * from './align/alignStack';
10+
export * from './align/alignDifferentSize';
11+
export * from './align/cropCommonArea';

0 commit comments

Comments
 (0)