Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start implementing stacks #416

Merged
merged 23 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions src/Stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { BitDepth } from 'fast-png';

import { Image } from './Image';
import { maxImage } from './stack/maxImage';
import { meanImage } from './stack/meanImage';
import { medianImage } from './stack/medianImage';
import { minImage } from './stack/minImage';
import {
checkImagesValid,
verifySameDimensions,
} from './stack/utils/checkImagesValid';
import { ImageColorModel } from './utils/constants/colorModels';

export class Stack {
/**
* The array of images.
*/
private readonly images: Image[];
/**
* The stack size.
*/
public readonly size: number;
/**
* Do the images have an alpha channel?
*/
public readonly alpha: boolean;
/**
* The color model of the images.
*/
public readonly colorModel: ImageColorModel;
/**
* The bit depth of the images.
*/
public readonly bitDepth: BitDepth;
/**
* Whether all the images of the stack have the same dimensions.
*/
public readonly sameDimensions: boolean;
/**
* The number of channels of the images.
*/
public readonly channels: number;

/**
* Create a new stack from an array of images.
* The images must have the same bit depth and color model.
* @param images - Array of images from which to create the stack.
*/
public constructor(images: Image[]) {
checkImagesValid(images);
this.images = images;
this.size = images.length;
this.alpha = images[0].alpha;
this.colorModel = images[0].colorModel;
this.channels = images[0].channels;
this.bitDepth = images[0].bitDepth;
this.sameDimensions = verifySameDimensions(images);
}

*[Symbol.iterator](): IterableIterator<Image> {
for (const image of this.images) {
yield image;
}
}

/**
* Clone a stack.
* @returns A new stack with the same images.
*/
public clone(): Stack {
return new Stack(this.images.map((image) => image.clone()));
}

/**
* Get the images of the stack. Mainly for debugging purposes.
* @returns The images.
*/
public getImages(): Image[] {
return this.images;
}

/**
* Get the image at the given index.
* @param index - The index of the image.
* @returns The image.
*/
public getImage(index: number): Image {
return this.images[index];
}

/**
* Get a value from an image of the stack.
* @param stackIndex - Index of the image in the stack.
* @param row - Row index of the pixel.
* @param column - Column index of the pixel.
* @param channel - The channel to retrieve.
* @returns The value at the given position.
*/
public getValue(
stackIndex: number,
row: number,
column: number,
channel: number,
): number {
return this.images[stackIndex].getValue(row, column, channel);
}

/**
* Get a value from an image of the stack. Specify the pixel position using its index.
* @param stackIndex - Index of the image in the stack.
* @param index - The index of the pixel.
* @param channel - The channel to retrieve.
* @returns The value at the given position.
*/
public getValueByIndex(
stackIndex: number,
index: number,
channel: number,
): number {
return this.images[stackIndex].getValueByIndex(index, channel);
}

/**
* Return the image containing the minimum values of all the images in the stack for
* each pixel. All the images must have the same dimensions.
* @returns The minimum image.
*/
public minImage(): Image {
return minImage(this);
}

/**
* Return the image containing the maximum values of all the images in the stack for
* each pixel. All the images must have the same dimensions.
* @returns The maximum image.
*/
public maxImage(): Image {
return maxImage(this);
}

/**
* Return the image containing the median values of all the images in the stack for
* each pixel. All the images must have the same dimensions.
* @returns The median image.
*/
public medianImage(): Image {
return medianImage(this);
}

/**
* Return the image containing the average values of all the images in the stack for
* each pixel. All the images must have the same dimensions.
* @returns The mean image.
*/
public meanImage(): Image {
return meanImage(this);
}

/**
* Get the global histogram of the stack.
*/
// public getHistogram(): Uint32Array {}

/**
* Align all the images of the stack on the image at the given index.
* @param refIndex - The index of the reference image.
*/
// public alignImages(refIndex: number): Stack {}

/**
* Map a function on all the images of the stack.
* @param callback - Function to apply on each image.
* @returns New stack with the modified images.
*/
public map(callback: (image: Image) => Image): Stack {
return new Stack(this.images.map(callback));
}

/**
* Filter the images in the stack.
* @param callback - Function to decide which images to keep.
* @returns New filtered stack.
*/
public filter(callback: (image: Image) => boolean): Stack {
return new Stack(this.images.filter(callback));
}
}
105 changes: 105 additions & 0 deletions src/__tests__/Stack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Image } from '../Image';
import { Stack } from '../Stack';

describe('Stack constructor', () => {
it('create a stack containing one image', () => {
const image = testUtils.createGreyImage([[1, 2, 3, 4]]);
const stack = new Stack([image]);

const images = stack.getImages();
expect(stack).toBeInstanceOf(Stack);
expect(images).toHaveLength(1);
expect(images[0]).toBeInstanceOf(Image);
expect(images[0]).toBe(image);
expect(stack.size).toBe(1);
expect(stack.alpha).toBe(false);
expect(stack.colorModel).toBe('GREY');
expect(stack.channels).toBe(1);
expect(stack.bitDepth).toBe(8);
expect(stack.sameDimensions).toBe(true);
});

it('should throw if color model is different', () => {
const image1 = testUtils.createGreyImage([[1, 2, 3, 4]]);
const image2 = testUtils.createRgbaImage([[1, 2, 3, 4]]);
expect(() => {
return new Stack([image1, image2]);
}).toThrow('images must all have the same bit depth and color model');
});

it('should throw if bit depths different', () => {
const image1 = testUtils.createGreyImage([[1, 2, 3, 4]], { bitDepth: 8 });
const image2 = testUtils.createGreyImage([[1, 2, 3, 4]], { bitDepth: 16 });
expect(() => {
return new Stack([image1, image2]);
}).toThrow('images must all have the same bit depth and color model');
});
});

test('iterator', () => {
expect.assertions(2);
const image = testUtils.createGreyImage([[1, 2, 3, 4]]);
const stack = new Stack([image, image]);

for (const image of stack) {
expect(image).toBeInstanceOf(Image);
}
});

test('clone', () => {
const image = testUtils.createGreyImage([[1, 2, 3, 4]]);
const stack = new Stack([image]);
const clone = stack.clone();
expect(clone).toBeInstanceOf(Stack);
expect(clone).not.toBe(stack);
expect(clone.getImages()[0]).toBeInstanceOf(Image);
expect(clone.getImages()[0]).not.toBe(image);
expect(clone.getImages()[0]).toEqual(image);
});

test('getImage', () => {
const image = testUtils.createGreyImage([[1, 2, 3, 4]]);
const stack = new Stack([image]);
expect(stack.getImage(0)).toBe(image);
});

describe('get values from stack', () => {
it('getValue on grey image', () => {
const image = testUtils.createGreyImage([[1, 2, 3, 4]]);
const stack = new Stack([image]);
expect(stack.getValue(0, 0, 0, 0)).toBe(1);
});

it('getValue on RGB image', () => {
const image1 = testUtils.createRgbImage([[1, 2, 3]]);
const image2 = testUtils.createRgbImage([[4, 5, 6]]);
const stack = new Stack([image1, image2]);
expect(stack.getValue(1, 0, 0, 1)).toBe(5);
});

it('getValueByIndex', () => {
const image = testUtils.createGreyImage([[1, 2, 3, 4]]);
const stack = new Stack([image]);
expect(stack.getValueByIndex(0, 1, 0)).toBe(2);
});
});

test('level the images with map', () => {
const image1 = testUtils.createGreyImage([[1, 2, 3, 4]]);
const image2 = testUtils.createGreyImage([[4, 5, 6, 7]]);
const stack = new Stack([image1, image2]);
const result = stack.map((image) => image.level());
expect(result).not.toBe(stack);
expect(result.getImage(0)).toMatchImageData([[0, 85, 170, 255]]);
expect(result.getImage(1)).toMatchImageData([[0, 85, 170, 255]]);
});

test('remove images too dark using filter', () => {
const image1 = testUtils.createGreyImage([[1, 2, 3, 4]]);
const image2 = testUtils.createGreyImage([[100, 100, 100, 100]]);
const stack = new Stack([image1, image2]);
const result = stack.filter((image) => image.mean()[0] > 10);
expect(result).not.toBe(stack);
expect(result.size).toBe(1);
expect(result.getImage(0)).toMatchImageData([[100, 100, 100, 100]]);
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions src/stack/__tests__/maxImage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { join } from 'node:path';

import { Image } from '../../Image';
import { Stack } from '../../Stack';
import { getStackFromFolder } from '../utils/getStackFromFolder';

test('2 grey images', () => {
const image1 = testUtils.createGreyImage([[1, 2, 3, 4]]);
const image2 = testUtils.createGreyImage([[4, 3, 2, 1]]);
const stack = new Stack([image1, image2]);
const maxImage = stack.maxImage();

expect(maxImage).toBeInstanceOf(Image);
expect(maxImage.width).toBe(4);
expect(maxImage.height).toBe(1);
expect(maxImage.channels).toBe(1);
expect(maxImage).toMatchImageData([[4, 3, 3, 4]]);
});

test('more complex stack', () => {
const folder = join(__dirname, '../../../test/img/correctColor');
const stack = getStackFromFolder(folder);

expect(stack.maxImage()).toMatchImageSnapshot();
});

test('2 RGB images', () => {
const image1 = testUtils.createRgbImage([[1, 2, 10]]);
const image2 = testUtils.createRgbImage([[5, 6, 7]]);
const stack = new Stack([image1, image2]);
const maxImage = stack.maxImage();

expect(maxImage).toBeInstanceOf(Image);
expect(maxImage.width).toBe(1);
expect(maxImage.height).toBe(1);
expect(maxImage.channels).toBe(3);
expect(maxImage).toMatchImageData([[5, 6, 10]]);
});
49 changes: 49 additions & 0 deletions src/stack/__tests__/meanImage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { join } from 'node:path';

import { Image } from '../../Image';
import { Stack } from '../../Stack';
import { getStackFromFolder } from '../utils/getStackFromFolder';

test('2 grey images', () => {
const image1 = testUtils.createGreyImage([[1, 2, 3, 4]]);
const image2 = testUtils.createGreyImage([[4, 3, 2, 1]]);
const stack = new Stack([image1, image2]);
const meanImage = stack.meanImage();

expect(meanImage).toBeInstanceOf(Image);
expect(meanImage.width).toBe(4);
expect(meanImage.height).toBe(1);
expect(meanImage.channels).toBe(1);
expect(meanImage).toMatchImageData([[2, 2, 2, 2]]);
});

test('2 RGB images', () => {
const image1 = testUtils.createRgbImage([[1, 2, 3]]);
const image2 = testUtils.createRgbImage([[5, 6, 7]]);
const stack = new Stack([image1, image2]);
const meanImage = stack.meanImage();

expect(meanImage).toBeInstanceOf(Image);
expect(meanImage.width).toBe(1);
expect(meanImage.height).toBe(1);
expect(meanImage.channels).toBe(3);
expect(meanImage).toMatchImageData([[3, 4, 5]]);
});

test('more complex stack', () => {
const folder = join(__dirname, '../../../test/img/correctColor');
const stack = getStackFromFolder(folder);
expect(stack.meanImage()).toMatchImageSnapshot();
});

test('2 grey images 16 bits depth', () => {
const data = new Uint16Array([1, 2, 3, 4]);
const image1 = new Image(4, 1, { data, bitDepth: 16, colorModel: 'GREY' });
const image2 = new Image(4, 1, { data, bitDepth: 16, colorModel: 'GREY' });
const stack = new Stack([image1, image2]);
const meanImage = stack.meanImage();

expect(meanImage).toBeInstanceOf(Image);
expect(meanImage.bitDepth).toBe(16);
expect(meanImage).toMatchImage(image1);
});
Loading
Loading