Skip to content
This repository was archived by the owner on Jul 26, 2025. It is now read-only.

Commit 37db7e3

Browse files
feat: add options to mean, median and variance (#471)
1 parent ad9f91d commit 37db7e3

File tree

8 files changed

+337
-28
lines changed

8 files changed

+337
-28
lines changed

src/Image.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import { Mask } from './Mask';
44
import { add, subtract, SubtractImageOptions } from './compare';
55
import { divide, DivideOptions } from './compare/divide';
66
import { multiply, MultiplyOptions } from './compare/multiply';
7-
import { median } from './compute';
8-
import { variance } from './compute/variance';
7+
import {
8+
MedianOptions,
9+
MeanOptions,
10+
median,
11+
VarianceOptions,
12+
variance,
13+
} from './compute';
914
import { correctColor } from './correctColor';
1015
import {
1116
drawCircleOnImage,
@@ -707,26 +712,29 @@ export class Image {
707712

708713
/**
709714
* Compute the mean pixel of an image.
715+
* @param options - Mean options.
710716
* @returns The mean pixel.
711717
*/
712-
public mean(): number[] {
713-
return mean(this);
718+
public mean(options?: MeanOptions): number[] {
719+
return mean(this, options);
714720
}
715721

716722
/**
717723
* Compute the median pixel of an image.
724+
* @param options - Median options.
718725
* @returns The median pixel.
719726
*/
720-
public median(): number[] {
721-
return median(this);
727+
public median(options?: MedianOptions): number[] {
728+
return median(this, options);
722729
}
723730

724731
/**
725732
* Compute the variance of each channel of an image.
733+
* @param options - Variance options.
726734
* @returns The variance of the channels of the image.
727735
*/
728-
public variance(): number[] {
729-
return variance(this);
736+
public variance(options?: VarianceOptions): number[] {
737+
return variance(this, options);
730738
}
731739

732740
// DRAW

src/compute/__tests__/mean.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Point } from '../../geometry';
12
import { mean } from '../mean';
23

34
test('5x1 RGB image', () => {
@@ -38,3 +39,78 @@ test('2x4 GREY image', () => {
3839

3940
expect(result).toStrictEqual([1.5]);
4041
});
42+
test('mean from points', () => {
43+
const image = testUtils.createGreyImage([
44+
[1, 2, 3, 0],
45+
[1, 2, 3, 0],
46+
]);
47+
const points = [
48+
{ column: 0, row: 0 },
49+
{ column: 1, row: 0 },
50+
{ column: 2, row: 1 },
51+
];
52+
const result = image.mean({ points });
53+
54+
expect(result).toStrictEqual([2]);
55+
});
56+
test('mean from points in rgba image', () => {
57+
const image = testUtils.createRgbaImage([
58+
[1, 2, 3, 0],
59+
[1, 2, 3, 0],
60+
]);
61+
const points = [
62+
{ column: 0, row: 0 },
63+
{ column: 0, row: 1 },
64+
];
65+
const result = image.mean({ points });
66+
67+
expect(result).toStrictEqual([1, 2, 3, 0]);
68+
});
69+
test('must throw if array is empty', () => {
70+
const image = testUtils.createRgbaImage([
71+
[1, 2, 3, 0],
72+
[1, 2, 3, 0],
73+
]);
74+
const points: Point[] = [];
75+
76+
expect(() => {
77+
const result = image.mean({ points });
78+
return result;
79+
}).toThrow('Array of coordinates is empty.');
80+
});
81+
test("must throw if point's row is invalid.", () => {
82+
const image = testUtils.createRgbaImage([
83+
[1, 2, 3, 0],
84+
[1, 2, 3, 0],
85+
]);
86+
const points: Point[] = [{ column: 0, row: 2 }];
87+
88+
expect(() => {
89+
const result = image.mean({ points });
90+
return result;
91+
}).toThrow('Invalid coordinate: {column: 0, row: 2}');
92+
});
93+
test("must throw if point's column is invalid.", () => {
94+
const image = testUtils.createRgbaImage([
95+
[1, 2, 3, 0],
96+
[1, 2, 3, 0],
97+
]);
98+
const points: Point[] = [{ column: 4, row: 1 }];
99+
100+
expect(() => {
101+
const result = image.mean({ points });
102+
return result;
103+
}).toThrow('Invalid coordinate: {column: 4, row: 1}');
104+
});
105+
test('must throw if point has negative values.', () => {
106+
const image = testUtils.createRgbaImage([
107+
[1, 2, 3, 0],
108+
[1, 2, 3, 0],
109+
]);
110+
const points: Point[] = [{ column: -14, row: 0 }];
111+
112+
expect(() => {
113+
const result = image.mean({ points });
114+
return result;
115+
}).toThrow('Invalid coordinate: {column: -14, row: 0}');
116+
});

src/compute/__tests__/median.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Point } from '../../geometry';
12
import { median } from '../median';
23

34
test('5x1 RGB image', () => {
@@ -38,3 +39,82 @@ test('2x4 GREY image', () => {
3839

3940
expect(result).toStrictEqual([2]);
4041
});
42+
43+
test('median from points', () => {
44+
const image = testUtils.createGreyImage([
45+
[1, 2, 2, 2],
46+
[1, 2, 3, 2],
47+
]);
48+
const points = [
49+
{ column: 0, row: 0 },
50+
{ column: 2, row: 1 },
51+
{ column: 1, row: 0 },
52+
];
53+
54+
const result = image.median({ points });
55+
56+
expect(result).toStrictEqual([2]);
57+
});
58+
59+
test('median from points on rgba image', () => {
60+
const image = testUtils.createRgbaImage([
61+
[1, 2, 2, 2],
62+
[1, 2, 3, 2],
63+
]);
64+
const points = [
65+
{ column: 0, row: 0 },
66+
{ column: 0, row: 1 },
67+
];
68+
69+
const result = image.median({ points });
70+
71+
expect(result).toStrictEqual([1, 2, 2, 2]);
72+
});
73+
74+
test('must throw if array is empty', () => {
75+
const image = testUtils.createRgbaImage([
76+
[1, 2, 2, 2],
77+
[1, 2, 3, 2],
78+
]);
79+
const points: Point[] = [];
80+
expect(() => {
81+
const result = image.median({ points });
82+
return result;
83+
}).toThrow('Array of coordinates is empty.');
84+
});
85+
86+
test("must throw if point's row is invalid", () => {
87+
const image = testUtils.createRgbaImage([
88+
[1, 2, 2, 2],
89+
[1, 2, 3, 2],
90+
]);
91+
const points: Point[] = [{ column: 0, row: 2 }];
92+
expect(() => {
93+
const result = image.median({ points });
94+
return result;
95+
}).toThrow('Invalid coordinate: {column: 0, row: 2}');
96+
});
97+
98+
test("must throw if point's column is invalid", () => {
99+
const image = testUtils.createRgbaImage([
100+
[1, 2, 2, 2],
101+
[1, 2, 3, 2],
102+
]);
103+
const points: Point[] = [{ column: 4, row: 1 }];
104+
expect(() => {
105+
const result = image.median({ points });
106+
return result;
107+
}).toThrow('Invalid coordinate: {column: 4, row: 1}');
108+
});
109+
test('must throw if point has negative values.', () => {
110+
const image = testUtils.createRgbaImage([
111+
[1, 2, 3, 0],
112+
[1, 2, 3, 0],
113+
]);
114+
const points: Point[] = [{ column: -14, row: 0 }];
115+
116+
expect(() => {
117+
const result = image.mean({ points });
118+
return result;
119+
}).toThrow('Invalid coordinate: {column: -14, row: 0}');
120+
});

src/compute/__tests__/variance.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Point } from '../../geometry';
12
import { variance } from '../variance';
23

34
test('1x1 RGB image', () => {
@@ -16,3 +17,66 @@ test('GREY image', () => {
1617

1718
expect(result).toStrictEqual([525]);
1819
});
20+
21+
test('variance from points', () => {
22+
const image = testUtils.createGreyImage([
23+
[10, 20, 30, 40],
24+
[50, 60, 70, 80],
25+
]);
26+
27+
const points = [
28+
{ column: 0, row: 0 },
29+
{ column: 1, row: 0 },
30+
{ column: 2, row: 0 },
31+
{ column: 3, row: 0 },
32+
];
33+
34+
const result = image.variance({ points });
35+
36+
expect(result).toStrictEqual([125]);
37+
});
38+
test('must throw if array is empty', () => {
39+
const image = testUtils.createRgbaImage([
40+
[1, 2, 2, 2],
41+
[1, 2, 3, 2],
42+
]);
43+
const points: Point[] = [];
44+
expect(() => {
45+
const result = image.median({ points });
46+
return result;
47+
}).toThrow('Array of coordinates is empty.');
48+
});
49+
test("must throw if point's coordinates are invalid", () => {
50+
const image = testUtils.createGreyImage([
51+
[1, 2, 2, 2],
52+
[1, 2, 3, 2],
53+
]);
54+
const points: Point[] = [{ column: 0, row: 2 }];
55+
expect(() => {
56+
const result = image.median({ points });
57+
return result;
58+
}).toThrow('Invalid coordinate: {column: 0, row: 2}');
59+
});
60+
test("must throw if point's coordinates are invalid", () => {
61+
const image = testUtils.createGreyImage([
62+
[1, 2, 2, 2],
63+
[1, 2, 3, 2],
64+
]);
65+
const points: Point[] = [{ column: 4, row: 1 }];
66+
expect(() => {
67+
const result = image.median({ points });
68+
return result;
69+
}).toThrow('Invalid coordinate: {column: 4, row: 1}');
70+
});
71+
test('must throw if point has negative values.', () => {
72+
const image = testUtils.createRgbaImage([
73+
[1, 2, 3, 0],
74+
[1, 2, 3, 0],
75+
]);
76+
const points: Point[] = [{ column: -14, row: 0 }];
77+
78+
expect(() => {
79+
const result = image.mean({ points });
80+
return result;
81+
}).toThrow('Invalid coordinate: {column: -14, row: 0}');
82+
});

src/compute/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './mean';
22
export * from './histogram';
33
export * from './median';
44
export * from './getExtrema';
5+
export * from './variance';

src/compute/mean.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,48 @@
11
import { Image } from '../Image';
2+
import { Point } from '../geometry';
3+
4+
export interface MeanOptions {
5+
/**
6+
* Points to calculate mean from.
7+
*/
8+
points: Point[];
9+
}
210

311
/**
412
* Compute the mean of an image. The mean can be either computed on each channel
513
* individually or on the whole image.
614
* @param image - Image to process.
15+
* @param options - Mean options.
716
* @returns The mean pixel.
817
*/
9-
export function mean(image: Image): number[] {
10-
const pixel = new Array<number>(image.channels).fill(0);
11-
for (let row = 0; row < image.height; row++) {
12-
for (let column = 0; column < image.width; column++) {
18+
export function mean(image: Image, options?: MeanOptions): number[] {
19+
const pixelSum = new Array<number>(image.channels).fill(0);
20+
const nbValues = options ? options.points.length : image.size;
21+
if (nbValues === 0) throw new RangeError('Array of coordinates is empty.');
22+
if (options) {
23+
for (const point of options.points) {
1324
for (let channel = 0; channel < image.channels; channel++) {
14-
pixel[channel] += image.getValue(column, row, channel);
25+
if (
26+
point.column < 0 ||
27+
point.column >= image.width ||
28+
point.row < 0 ||
29+
point.row >= image.height
30+
) {
31+
throw new RangeError(
32+
`Invalid coordinate: {column: ${point.column}, row: ${point.row}}.`,
33+
);
34+
}
35+
pixelSum[channel] += image.getValueByPoint(point, channel);
36+
}
37+
}
38+
} else {
39+
for (let row = 0; row < image.height; row++) {
40+
for (let column = 0; column < image.width; column++) {
41+
for (let channel = 0; channel < image.channels; channel++) {
42+
pixelSum[channel] += image.getValue(column, row, channel);
43+
}
1544
}
1645
}
1746
}
18-
return pixel.map((channel) => channel / image.size);
47+
return pixelSum.map((channelSum) => channelSum / nbValues);
1948
}

0 commit comments

Comments
 (0)