Skip to content

Commit

Permalink
feat: add options to mean, median and variance (#471)
Browse files Browse the repository at this point in the history
  • Loading branch information
EscapedGibbon authored Jun 14, 2024
1 parent ad9f91d commit 37db7e3
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 28 deletions.
24 changes: 16 additions & 8 deletions src/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { Mask } from './Mask';
import { add, subtract, SubtractImageOptions } from './compare';
import { divide, DivideOptions } from './compare/divide';
import { multiply, MultiplyOptions } from './compare/multiply';
import { median } from './compute';
import { variance } from './compute/variance';
import {
MedianOptions,
MeanOptions,
median,
VarianceOptions,
variance,
} from './compute';
import { correctColor } from './correctColor';
import {
drawCircleOnImage,
Expand Down Expand Up @@ -707,26 +712,29 @@ export class Image {

/**
* Compute the mean pixel of an image.
* @param options - Mean options.
* @returns The mean pixel.
*/
public mean(): number[] {
return mean(this);
public mean(options?: MeanOptions): number[] {
return mean(this, options);
}

/**
* Compute the median pixel of an image.
* @param options - Median options.
* @returns The median pixel.
*/
public median(): number[] {
return median(this);
public median(options?: MedianOptions): number[] {
return median(this, options);
}

/**
* Compute the variance of each channel of an image.
* @param options - Variance options.
* @returns The variance of the channels of the image.
*/
public variance(): number[] {
return variance(this);
public variance(options?: VarianceOptions): number[] {
return variance(this, options);
}

// DRAW
Expand Down
76 changes: 76 additions & 0 deletions src/compute/__tests__/mean.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Point } from '../../geometry';
import { mean } from '../mean';

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

expect(result).toStrictEqual([1.5]);
});
test('mean from points', () => {
const image = testUtils.createGreyImage([
[1, 2, 3, 0],
[1, 2, 3, 0],
]);
const points = [
{ column: 0, row: 0 },
{ column: 1, row: 0 },
{ column: 2, row: 1 },
];
const result = image.mean({ points });

expect(result).toStrictEqual([2]);
});
test('mean from points in rgba image', () => {
const image = testUtils.createRgbaImage([
[1, 2, 3, 0],
[1, 2, 3, 0],
]);
const points = [
{ column: 0, row: 0 },
{ column: 0, row: 1 },
];
const result = image.mean({ points });

expect(result).toStrictEqual([1, 2, 3, 0]);
});
test('must throw if array is empty', () => {
const image = testUtils.createRgbaImage([
[1, 2, 3, 0],
[1, 2, 3, 0],
]);
const points: Point[] = [];

expect(() => {
const result = image.mean({ points });
return result;
}).toThrow('Array of coordinates is empty.');
});
test("must throw if point's row is invalid.", () => {
const image = testUtils.createRgbaImage([
[1, 2, 3, 0],
[1, 2, 3, 0],
]);
const points: Point[] = [{ column: 0, row: 2 }];

expect(() => {
const result = image.mean({ points });
return result;
}).toThrow('Invalid coordinate: {column: 0, row: 2}');
});
test("must throw if point's column is invalid.", () => {
const image = testUtils.createRgbaImage([
[1, 2, 3, 0],
[1, 2, 3, 0],
]);
const points: Point[] = [{ column: 4, row: 1 }];

expect(() => {
const result = image.mean({ points });
return result;
}).toThrow('Invalid coordinate: {column: 4, row: 1}');
});
test('must throw if point has negative values.', () => {
const image = testUtils.createRgbaImage([
[1, 2, 3, 0],
[1, 2, 3, 0],
]);
const points: Point[] = [{ column: -14, row: 0 }];

expect(() => {
const result = image.mean({ points });
return result;
}).toThrow('Invalid coordinate: {column: -14, row: 0}');
});
80 changes: 80 additions & 0 deletions src/compute/__tests__/median.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Point } from '../../geometry';
import { median } from '../median';

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

expect(result).toStrictEqual([2]);
});

test('median from points', () => {
const image = testUtils.createGreyImage([
[1, 2, 2, 2],
[1, 2, 3, 2],
]);
const points = [
{ column: 0, row: 0 },
{ column: 2, row: 1 },
{ column: 1, row: 0 },
];

const result = image.median({ points });

expect(result).toStrictEqual([2]);
});

test('median from points on rgba image', () => {
const image = testUtils.createRgbaImage([
[1, 2, 2, 2],
[1, 2, 3, 2],
]);
const points = [
{ column: 0, row: 0 },
{ column: 0, row: 1 },
];

const result = image.median({ points });

expect(result).toStrictEqual([1, 2, 2, 2]);
});

test('must throw if array is empty', () => {
const image = testUtils.createRgbaImage([
[1, 2, 2, 2],
[1, 2, 3, 2],
]);
const points: Point[] = [];
expect(() => {
const result = image.median({ points });
return result;
}).toThrow('Array of coordinates is empty.');
});

test("must throw if point's row is invalid", () => {
const image = testUtils.createRgbaImage([
[1, 2, 2, 2],
[1, 2, 3, 2],
]);
const points: Point[] = [{ column: 0, row: 2 }];
expect(() => {
const result = image.median({ points });
return result;
}).toThrow('Invalid coordinate: {column: 0, row: 2}');
});

test("must throw if point's column is invalid", () => {
const image = testUtils.createRgbaImage([
[1, 2, 2, 2],
[1, 2, 3, 2],
]);
const points: Point[] = [{ column: 4, row: 1 }];
expect(() => {
const result = image.median({ points });
return result;
}).toThrow('Invalid coordinate: {column: 4, row: 1}');
});
test('must throw if point has negative values.', () => {
const image = testUtils.createRgbaImage([
[1, 2, 3, 0],
[1, 2, 3, 0],
]);
const points: Point[] = [{ column: -14, row: 0 }];

expect(() => {
const result = image.mean({ points });
return result;
}).toThrow('Invalid coordinate: {column: -14, row: 0}');
});
64 changes: 64 additions & 0 deletions src/compute/__tests__/variance.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Point } from '../../geometry';
import { variance } from '../variance';

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

expect(result).toStrictEqual([525]);
});

test('variance from points', () => {
const image = testUtils.createGreyImage([
[10, 20, 30, 40],
[50, 60, 70, 80],
]);

const points = [
{ column: 0, row: 0 },
{ column: 1, row: 0 },
{ column: 2, row: 0 },
{ column: 3, row: 0 },
];

const result = image.variance({ points });

expect(result).toStrictEqual([125]);
});
test('must throw if array is empty', () => {
const image = testUtils.createRgbaImage([
[1, 2, 2, 2],
[1, 2, 3, 2],
]);
const points: Point[] = [];
expect(() => {
const result = image.median({ points });
return result;
}).toThrow('Array of coordinates is empty.');
});
test("must throw if point's coordinates are invalid", () => {
const image = testUtils.createGreyImage([
[1, 2, 2, 2],
[1, 2, 3, 2],
]);
const points: Point[] = [{ column: 0, row: 2 }];
expect(() => {
const result = image.median({ points });
return result;
}).toThrow('Invalid coordinate: {column: 0, row: 2}');
});
test("must throw if point's coordinates are invalid", () => {
const image = testUtils.createGreyImage([
[1, 2, 2, 2],
[1, 2, 3, 2],
]);
const points: Point[] = [{ column: 4, row: 1 }];
expect(() => {
const result = image.median({ points });
return result;
}).toThrow('Invalid coordinate: {column: 4, row: 1}');
});
test('must throw if point has negative values.', () => {
const image = testUtils.createRgbaImage([
[1, 2, 3, 0],
[1, 2, 3, 0],
]);
const points: Point[] = [{ column: -14, row: 0 }];

expect(() => {
const result = image.mean({ points });
return result;
}).toThrow('Invalid coordinate: {column: -14, row: 0}');
});
1 change: 1 addition & 0 deletions src/compute/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './mean';
export * from './histogram';
export * from './median';
export * from './getExtrema';
export * from './variance';
41 changes: 35 additions & 6 deletions src/compute/mean.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
import { Image } from '../Image';
import { Point } from '../geometry';

export interface MeanOptions {
/**
* Points to calculate mean from.
*/
points: Point[];
}

/**
* Compute the mean of an image. The mean can be either computed on each channel
* individually or on the whole image.
* @param image - Image to process.
* @param options - Mean options.
* @returns The mean pixel.
*/
export function mean(image: Image): number[] {
const pixel = new Array<number>(image.channels).fill(0);
for (let row = 0; row < image.height; row++) {
for (let column = 0; column < image.width; column++) {
export function mean(image: Image, options?: MeanOptions): number[] {
const pixelSum = new Array<number>(image.channels).fill(0);
const nbValues = options ? options.points.length : image.size;
if (nbValues === 0) throw new RangeError('Array of coordinates is empty.');
if (options) {
for (const point of options.points) {
for (let channel = 0; channel < image.channels; channel++) {
pixel[channel] += image.getValue(column, row, channel);
if (
point.column < 0 ||
point.column >= image.width ||
point.row < 0 ||
point.row >= image.height
) {
throw new RangeError(
`Invalid coordinate: {column: ${point.column}, row: ${point.row}}.`,
);
}
pixelSum[channel] += image.getValueByPoint(point, channel);
}
}
} else {
for (let row = 0; row < image.height; row++) {
for (let column = 0; column < image.width; column++) {
for (let channel = 0; channel < image.channels; channel++) {
pixelSum[channel] += image.getValue(column, row, channel);
}
}
}
}
return pixel.map((channel) => channel / image.size);
return pixelSum.map((channelSum) => channelSum / nbValues);
}
Loading

0 comments on commit 37db7e3

Please sign in to comment.