This commit is contained in:
Gašper Dobrovoljc 2023-04-29 17:27:08 +02:00
parent de93ee957b
commit 2ca44089b3
No known key found for this signature in database
GPG Key ID: 0E7E037018CFA5A5
26 changed files with 803 additions and 350 deletions

View File

@ -1,13 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>imgproc</title>
<script defer type="module" src="/src/main.ts"></script>
</head>
<body>
<body class="bg-white dark:bg-gray-800">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -13,6 +13,7 @@
"@popperjs/core": "^2.11.7",
"@sveltejs/vite-plugin-svelte": "^2.0.3",
"@tsconfig/svelte": "^3.0.0",
"@types/node": "^18.15.12",
"autoprefixer": "^10.4.7",
"chart.js": "^4.2.1",
"classnames": "^2.3.2",

View File

@ -10,6 +10,9 @@ devDependencies:
'@tsconfig/svelte':
specifier: ^3.0.0
version: 3.0.0
'@types/node':
specifier: ^18.15.12
version: 18.15.12
autoprefixer:
specifier: ^10.4.7
version: 10.4.7(postcss@8.4.21)
@ -60,7 +63,7 @@ devDependencies:
version: 4.9.3
vite:
specifier: ^4.2.0
version: 4.2.0
version: 4.2.0(@types/node@18.15.12)
packages:
@ -420,7 +423,7 @@ packages:
magic-string: 0.29.0
svelte: 3.55.1
svelte-hmr: 0.15.1(svelte@3.55.1)
vite: 4.2.0
vite: 4.2.0(@types/node@18.15.12)
vitefu: 0.2.4(vite@4.2.0)
transitivePeerDependencies:
- supports-color
@ -430,6 +433,10 @@ packages:
resolution: {integrity: sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==}
dev: true
/@types/node@18.15.12:
resolution: {integrity: sha512-Wha1UwsB3CYdqUm2PPzh/1gujGCNtWVUYF0mB00fJFoR4gTyWTDPjSm+zBF787Ahw8vSGgBja90MkgFwvB86Dg==}
dev: true
/@types/pug@2.0.6:
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
dev: true
@ -1483,7 +1490,7 @@ packages:
dev: true
optional: true
/vite@4.2.0:
/vite@4.2.0(@types/node@18.15.12):
resolution: {integrity: sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
@ -1508,6 +1515,7 @@ packages:
terser:
optional: true
dependencies:
'@types/node': 18.15.12
esbuild: 0.17.15
postcss: 8.4.21
resolve: 1.22.2
@ -1524,7 +1532,7 @@ packages:
vite:
optional: true
dependencies:
vite: 4.2.0
vite: 4.2.0(@types/node@18.15.12)
dev: true
/wrappy@1.0.2:

View File

@ -1,84 +1,189 @@
<script lang="ts">
import { Button, ButtonGroup, Fileupload, Spinner } from 'flowbite-svelte';
import { applyFilter, getHistogram } from './lib/processor';
import { Button, ButtonGroup, Spinner, Range, P } from 'flowbite-svelte';
import {
applyFilter,
grayscale,
threshold as threshold,
} from './lib/engine/processor';
import {
boxFilter,
sharpening,
type FilterFn,
edgeDetection,
sobel,
} from './lib/filters';
import { Line } from 'svelte-chartjs';
sharpeningFilter,
laplaceFilter,
sobelFilter,
gaussianBlurFilter,
boxBlurFilter,
removeColorChannelFilter,
thresholdFilter,
enhanceColorChannelFilter,
setBrightnessFilter,
} from './lib/engine/filters';
import 'chart.js/auto';
import ImageUpload from './lib/components/ImageUpload.svelte';
import demoImage from './assets/image.jpeg';
import { ColorChannel } from './lib/engine/types';
let files: FileList;
let originalSrc: string;
let filterSrc: string;
let originalSrc: string | null = demoImage;
let src: string | null = demoImage;
let processing = false;
let histogram: { red: number[]; green: number[]; blue: number[] };
$: if (files) {
originalSrc = URL.createObjectURL(files[0]);
getHistogram(originalSrc).then((h) => (histogram = h));
let thresholdValue = 128;
let brightnessValue = 1;
type Filter = {
name: string;
src: string;
};
let filters: Filter[] = [];
async function apply(name: string, filter: FilterFn) {
processing = true;
src = await applyFilter(src, filter);
filters = [...filters, { name, src }];
processing = false;
}
async function apply(filter: FilterFn) {
processing = true;
filterSrc = await applyFilter(originalSrc, filter);
processing = false;
function handleUpload(e: CustomEvent) {
src = e.detail.src;
originalSrc = e.detail.src;
}
function undo() {
filters = filters.slice(0, -1);
src = filters[filters.length - 1]?.src || originalSrc;
}
function reset() {
filters = [];
src = originalSrc;
}
</script>
<div class="p-4 space-y-2 w-screen">
<Fileupload bind:files />
<ButtonGroup>
<Button on:click={() => apply(boxFilter(2))}>Box Filter</Button>
<Button on:click={() => apply(sharpening)}>Sharpening</Button>
<Button on:click={() => apply(edgeDetection)}>Edge detection</Button>
<Button on:click={() => apply(sobel)}>Sobel</Button>
</ButtonGroup>
<div class="flex space-x-2 w-full">
<img src={originalSrc} alt="" class="w-1/2" />
{#if processing}
<div class="flex items-center justify-center w-1/2">
<Spinner />
</div>
{:else if filterSrc}
<img src={filterSrc} alt="" class="w-1/2" />
{/if}
<div class="p-4 space-y-2 h-screen flex flex-col">
<div class="w-max">
<ImageUpload on:upload={handleUpload} />
</div>
<div class="w-1/2">
<Line
data={{
labels: new Array(256).fill(0).map((_, i) => i),
datasets: [
{
fill: true,
label: 'Red',
borderColor: 'red',
backgroundColor: 'rgba(255, 0, 0, 0.2)',
data: histogram?.red,
},
{
fill: true,
label: 'Green',
borderColor: 'green',
backgroundColor: 'rgba(0, 255, 0, 0.2)',
data: histogram?.green,
},
{
fill: true,
label: 'Blue',
borderColor: 'blue',
backgroundColor: 'rgba(0, 0, 255, 0.2)',
data: histogram?.blue,
},
],
}}
/>
{#if src}
<P>Filters</P>
<div class="flex">
<ButtonGroup>
<Button on:click={() => apply('Box blur', boxBlurFilter)}
>Box blur</Button
>
<Button on:click={() => apply('Gaussian blur', gaussianBlurFilter)}>
Gaussian blur
</Button>
<Button on:click={() => apply('Laplace', laplaceFilter)}>Laplace</Button
>
<Button on:click={() => apply('Sobel', sobelFilter)}>Sobel</Button>
<Button on:click={() => apply('Sharpening', sharpeningFilter)}>
Sharpening
</Button>
<Button on:click={() => apply('Grayscale', grayscale)}>
Grayscale
</Button>
</ButtonGroup>
<div class="grow" />
<div class="gap-y-2">
<Button on:click={undo}>Undo</Button>
<Button color="red" on:click={reset}>Reset</Button>
</div>
</div>
<div class="flex items-center gap-x-4 w-96">
<Button
on:click={() =>
apply(`Treshold: ${thresholdValue}`, thresholdFilter(thresholdValue))}
>
Threshold
</Button>
<P class="w-16">{thresholdValue}</P>
<Range bind:value={thresholdValue} min={0} max={255} />
</div>
<div>
<ButtonGroup>
<Button
color="red"
on:click={() =>
apply('Remove Red', removeColorChannelFilter(ColorChannel.red))}
>
Remove Red
</Button>
<Button
color="green"
on:click={() =>
apply('Remove Green', removeColorChannelFilter(ColorChannel.green))}
>
Remove Green
</Button>
<Button
color="blue"
on:click={() =>
apply('Remove Blue', removeColorChannelFilter(ColorChannel.blue))}
>
Remove Blue
</Button>
</ButtonGroup>
</div>
<div>
<ButtonGroup>
<Button
color="red"
on:click={() =>
apply('Enhance Red', enhanceColorChannelFilter(ColorChannel.red))}
>
Enhance Red
</Button>
<Button
color="green"
on:click={() =>
apply(
'Enhance Green',
enhanceColorChannelFilter(ColorChannel.green),
)}
>
Enhance Green
</Button>
<Button
color="blue"
on:click={() =>
apply('Enhance Blue', enhanceColorChannelFilter(ColorChannel.blue))}
>
Enhance Blue
</Button>
</ButtonGroup>
</div>
<div class="flex items-center gap-x-4 w-96">
<Button
on:click={() =>
apply(
`Brightness: ${brightnessValue}`,
setBrightnessFilter(brightnessValue),
)}
>
Brightness
</Button>
<P class="w-16">{brightnessValue}</P>
<Range bind:value={brightnessValue} min={0} max={5} step={0.1} />
</div>
{/if}
<div class="grow flex">
<div class="w-64" />
<div class="grow flex justify-center items-center">
<img {src} alt="" />
{#if processing}
<div class="absolute">
<Spinner />
</div>
{/if}
</div>
<div class="w-64">
{#each filters as filter}
<P>{filter.name}</P>
{/each}
</div>
</div>
</div>

BIN
src/assets/banana.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

BIN
src/assets/image.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { Line } from 'svelte-chartjs';
type HistogramData = {
red: number[];
green: number[];
blue: number[];
};
export let histogram: HistogramData;
</script>
<Line
data={{
labels: new Array(256).fill(0).map((_, i) => i),
datasets: [
{
fill: true,
label: 'Red',
borderColor: 'red',
backgroundColor: 'rgba(255, 0, 0, 0.2)',
data: histogram.red,
},
{
fill: true,
label: 'Green',
borderColor: 'green',
backgroundColor: 'rgba(0, 255, 0, 0.2)',
data: histogram.green,
},
{
fill: true,
label: 'Blue',
borderColor: 'blue',
backgroundColor: 'rgba(0, 0, 255, 0.2)',
data: histogram.blue,
},
],
}}
/>

View File

@ -0,0 +1,13 @@
<script lang="ts">
import { Fileupload } from 'flowbite-svelte';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function handleFileChange(e) {
const src = URL.createObjectURL(e.currentTarget.files[0]);
dispatch('upload', { src });
}
</script>
<Fileupload on:change={handleFileChange} />

101
src/lib/engine/filters.ts Normal file
View File

@ -0,0 +1,101 @@
import {
grayscale,
clone,
add,
applyMatrix,
removeColorChannel,
threshold,
enhanceColorChannel,
setBrightness,
} from './processor';
import type { ColorChannel } from './types';
export type FilterFn = (imageData: ImageData) => Promise<void> | void;
export const boxBlurFilter: FilterFn = async (imageData: ImageData) => {
await applyMatrix(imageData, {
matrix: [
[1, 1, 1],
[1, 1, 1],
[1, 1, 1],
],
div: 9,
});
};
export const gaussianBlurFilter: FilterFn = async (imageData: ImageData) => {
await applyMatrix(imageData, {
matrix: [
[1, 2, 1],
[2, 4, 2],
[1, 2, 1],
],
div: 16,
});
};
export const laplaceFilter: FilterFn = async (imageData: ImageData) => {
await grayscale(imageData);
await applyMatrix(imageData, {
matrix: [
[0, -1, 0],
[-1, 4, -1],
[0, -1, 0],
],
});
};
export const sobelFilter: FilterFn = async (imageData: ImageData) => {
await grayscale(imageData);
const data2 = clone(imageData);
await Promise.all([
applyMatrix(imageData, {
matrix: [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1],
],
}),
applyMatrix(data2, {
matrix: [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1],
],
}),
]);
await add(imageData, data2);
};
export const sharpeningFilter: FilterFn = async (imageData: ImageData) => {
await applyMatrix(imageData, {
matrix: [
[0, -1, 0],
[-1, 5, -1],
[0, -1, 0],
],
});
};
export const thresholdFilter =
(thresholdValue: number): FilterFn =>
(imageData: ImageData) =>
threshold(imageData, thresholdValue);
export const removeColorChannelFilter =
(colorChannel: ColorChannel): FilterFn =>
(imageData: ImageData) =>
removeColorChannel(imageData, colorChannel);
export const enhanceColorChannelFilter =
(colorChannel: ColorChannel): FilterFn =>
(imageData: ImageData) =>
enhanceColorChannel(imageData, colorChannel);
export const setBrightnessFilter =
(brightness: number): FilterFn =>
(imageData: ImageData) =>
setBrightness(imageData, brightness);

182
src/lib/engine/processor.ts Normal file
View File

@ -0,0 +1,182 @@
import type { FilterFn } from './filters';
import type { ColorChannel } from './types';
import type { AddParams } from './workers/add';
import type { ApplyMarixParams } from './workers/applyMatrix';
import type { EnhanceColorChannelParams } from './workers/enhanceColorChannel';
import type { HistogramData } from './workers/getHistogram';
import type { RemoveColorChannelParams } from './workers/removeColorChannel';
import type { SetBrightnessParams } from './workers/setBrightness';
import type { SubtractParams } from './workers/subtract';
import type { TresholdParams } from './workers/treshold';
export type Filter = {
matrix: number[][];
div?: number;
};
export async function applyFilter(
src: string,
filter: FilterFn,
): Promise<string> {
const image = await loadImage(src);
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
await filter(imageData);
ctx.putImageData(imageData, 0, 0);
const url = canvas.toDataURL();
canvas.remove();
return url;
}
export async function applyMatrix(imageData: ImageData, filter: Filter) {
const newImageData = await runInWorker<ApplyMarixParams, ImageData>(
'./workers/applyMatrix.ts',
{ imageData, filter },
);
imageData.data.set(newImageData.data);
}
async function loadImage(src: string) {
const image = new Image();
image.src = src;
return new Promise<HTMLImageElement>((res, rej) => {
image.onload = () => res(image);
image.onerror = rej;
});
}
async function getImageData(src: string): Promise<ImageData> {
const image = await loadImage(src);
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
return ctx.getImageData(0, 0, canvas.width, canvas.height);
}
export async function getHistogram(src: string) {
const imageData = await getImageData(src);
const histogram = await runInWorker<ImageData, HistogramData>(
'./workers/getHistogram.ts',
imageData,
);
return histogram;
}
async function runInWorker<I, O>(url: string, data: I) {
return new Promise<O>((res, rej) => {
if (!window.Worker) {
rej('Worker is not supported in this browser');
return;
}
const workerUrl = new URL(url, import.meta.url);
const worker = new Worker(workerUrl, { type: 'module' });
worker.onmessage = (e: MessageEvent<O>) => res(e.data);
worker.postMessage(data);
});
}
export async function grayscale(imageData: ImageData) {
const newImageData = await runInWorker<ImageData, ImageData>(
'./workers/grayscale.ts',
imageData,
);
imageData.data.set(newImageData.data);
}
export async function add(imageData1: ImageData, imageData2: ImageData) {
const res = await runInWorker<AddParams, AddParams>('./workers/add.ts', {
imageData1,
imageData2,
});
imageData1.data.set(res.imageData1.data);
imageData2.data.set(res.imageData2.data);
}
export async function subtract(imageData1: ImageData, imageData2: ImageData) {
const res = await runInWorker<SubtractParams, SubtractParams>(
'./workers/subtract.ts',
{
imageData1,
imageData2,
},
);
imageData1.data.set(res.imageData1.data);
imageData2.data.set(res.imageData2.data);
}
export function clone(imageData: ImageData): ImageData {
return {
width: imageData.width,
height: imageData.height,
colorSpace: imageData.colorSpace,
data: new Uint8ClampedArray(imageData.data),
};
}
export async function threshold(imageData: ImageData, threshold: number) {
const newImageData = await runInWorker<TresholdParams, ImageData>(
'./workers/treshold.ts',
{
imageData,
threshold,
},
);
imageData.data.set(newImageData.data);
}
export async function removeColorChannel(
imageData: ImageData,
colorChannel: ColorChannel,
) {
const newImageData = await runInWorker<RemoveColorChannelParams, ImageData>(
'./workers/removeColorChannel.ts',
{
imageData,
colorChannel,
},
);
imageData.data.set(newImageData.data);
}
export async function enhanceColorChannel(
imageData: ImageData,
colorChannel: ColorChannel,
) {
const newImageData = await runInWorker<EnhanceColorChannelParams, ImageData>(
'./workers/enhanceColorChannel.ts',
{
imageData,
colorChannel,
},
);
imageData.data.set(newImageData.data);
}
export async function setBrightness(imageData: ImageData, brightness: number) {
const newImageData = await runInWorker<SetBrightnessParams, ImageData>(
'./workers/setBrightness.ts',
{
imageData,
brightness,
},
);
imageData.data.set(newImageData.data);
}

49
src/lib/engine/proxy.ts Normal file
View File

@ -0,0 +1,49 @@
export type Pixel = {
r: number;
g: number;
b: number;
a: number;
};
export class ImageDataProxy {
constructor(private readonly imageData: ImageData) {}
public getPixel(x: number, y: number): Pixel {
const { data, width, height } = this.imageData;
if (x > width - 1) x = width - 1;
else if (x < 0) x = 0;
if (y > height - 1) y = height - 1;
else if (y < 0) y = 0;
const i = (y * width + x) * 4;
return {
r: data[i],
g: data[i + 1],
b: data[i + 2],
a: data[i + 3],
};
}
public setPixel(x: number, y: number, pixel: Pixel) {
const { data, width, height } = this.imageData;
if (x > width - 1 || y > height - 1) return;
if (pixel.r > 255) pixel.r = 255;
else if (pixel.r < 0) pixel.r = 0;
if (pixel.g > 255) pixel.g = 255;
else if (pixel.g < 0) pixel.g = 0;
if (pixel.b > 255) pixel.b = 255;
else if (pixel.b < 0) pixel.b = 0;
const i = (y * width + x) * 4;
data[i] = pixel.r;
data[i + 1] = pixel.g;
data[i + 2] = pixel.b;
data[i + 3] = pixel.a;
}
}

5
src/lib/engine/types.ts Normal file
View File

@ -0,0 +1,5 @@
export enum ColorChannel {
red = 0,
green = 1,
blue = 2,
}

8
src/lib/engine/worker.ts Normal file
View File

@ -0,0 +1,8 @@
type WorkerFn<T> = (data: any) => T;
export function registerWorker<T>(func: WorkerFn<T>) {
self.onmessage = (e: MessageEvent<T>) => {
const result = func(e.data);
self.postMessage(result);
};
}

View File

@ -0,0 +1,27 @@
import { ImageDataProxy } from '../proxy';
import { registerWorker } from '../worker';
export type AddParams = {
imageData1: ImageData;
imageData2: ImageData;
};
registerWorker(({ imageData1, imageData2 }: AddParams) => {
const data1 = new ImageDataProxy(imageData1);
const data2 = new ImageDataProxy(imageData2);
for (let y = 0; y < imageData1.height; y++) {
for (let x = 0; x < imageData1.width; x++) {
const pix1 = data1.getPixel(x, y);
const pix2 = data2.getPixel(x, y);
data1.setPixel(x, y, {
r: pix1.r + pix2.r,
g: pix1.g + pix2.g,
b: pix1.b + pix2.b,
a: pix1.a,
});
}
}
return { imageData1, imageData2 };
});

View File

@ -0,0 +1,48 @@
import { clone, type Filter } from '../processor';
import { ImageDataProxy, type Pixel } from '../proxy';
import { registerWorker } from '../worker';
export type ApplyMarixParams = {
imageData: ImageData;
filter: Filter;
};
registerWorker(({ imageData, filter }: ApplyMarixParams) => {
const { matrix, div = 1 } = filter;
const proxy = new ImageDataProxy(imageData);
const newImageData = clone(imageData);
const newProxy = new ImageDataProxy(newImageData);
const offset = (matrix.length - 1) / 2;
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const pix = proxy.getPixel(x, y);
const sum: Pixel = { r: 0, g: 0, b: 0, a: pix.a };
for (let my = 0; my < matrix.length; my++) {
for (let mx = 0; mx < matrix[0].length; mx++) {
const mpix = proxy.getPixel(x + mx - offset, y + my - offset);
const mult = matrix[my][mx];
sum.r += mpix.r * mult;
sum.g += mpix.g * mult;
sum.b += mpix.b * mult;
}
}
if (div !== 1) {
sum.r /= div;
sum.g /= div;
sum.b /= div;
}
newProxy.setPixel(x, y, sum);
}
}
return newImageData;
});
export {};

View File

@ -0,0 +1,17 @@
import type { ColorChannel } from '../types';
import { registerWorker } from '../worker';
export type EnhanceColorChannelParams = {
imageData: ImageData;
colorChannel: ColorChannel;
};
registerWorker(({ imageData, colorChannel }: EnhanceColorChannelParams) => {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i + colorChannel] = 255;
}
return imageData;
});
export {};

View File

@ -0,0 +1,29 @@
import { ImageDataProxy } from '../proxy';
import { registerWorker } from '../worker';
export type HistogramData = {
red: number[];
green: number[];
blue: number[];
};
registerWorker((imageData: ImageData): HistogramData => {
const data = new ImageDataProxy(imageData);
const red = new Array(256).fill(0);
const green = new Array(256).fill(0);
const blue = new Array(256).fill(0);
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const pix = data.getPixel(x, y);
red[pix.r]++;
green[pix.g]++;
blue[pix.b]++;
}
}
return { red, green, blue };
});
export {};

View File

@ -0,0 +1,14 @@
import { registerWorker } from '../worker';
registerWorker((imageData: ImageData) => {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const val = 0.299 * data[i] + 0.587 * data[i] + 0.114 * data[i];
data[i] = val;
data[i + 1] = val;
data[i + 2] = val;
}
return imageData;
});
export {};

View File

@ -0,0 +1,17 @@
import type { ColorChannel } from '../types';
import { registerWorker } from '../worker';
export type RemoveColorChannelParams = {
imageData: ImageData;
colorChannel: ColorChannel;
};
registerWorker(({ imageData, colorChannel }: RemoveColorChannelParams) => {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i + colorChannel] = 0;
}
return imageData;
});
export {};

View File

@ -0,0 +1,17 @@
import { registerWorker } from '../worker';
export type SetBrightnessParams = {
imageData: ImageData;
brightness: number;
};
registerWorker(({ imageData, brightness }: SetBrightnessParams) => {
const data = imageData.data;
for (let i = 0; i < data.length; i++) {
if ((i + 1) % 4 === 0) continue;
data[i] *= brightness;
}
return imageData;
});
export {};

View File

@ -0,0 +1,27 @@
import { ImageDataProxy } from '../proxy';
import { registerWorker } from '../worker';
export type SubtractParams = {
imageData1: ImageData;
imageData2: ImageData;
};
registerWorker(({ imageData1, imageData2 }: SubtractParams) => {
const data1 = new ImageDataProxy(imageData1);
const data2 = new ImageDataProxy(imageData2);
for (let y = 0; y < imageData1.height; y++) {
for (let x = 0; x < imageData1.width; x++) {
const pix1 = data1.getPixel(x, y);
const pix2 = data2.getPixel(x, y);
data1.setPixel(x, y, {
r: pix1.r - pix2.r,
g: pix1.g - pix2.g,
b: pix1.b - pix2.b,
a: pix1.a,
});
}
}
return { imageData1, imageData2 };
});

View File

@ -0,0 +1,20 @@
import { registerWorker } from '../worker';
export type TresholdParams = {
imageData: ImageData;
threshold: number;
};
registerWorker(({ imageData, threshold }: TresholdParams) => {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
let val = 0.299 * data[i] + 0.587 * data[i] + 0.114 * data[i];
val = val > threshold ? 255 : 0;
data[i] = val;
data[i + 1] = val;
data[i + 2] = val;
}
return imageData;
});
export {};

View File

@ -1,66 +0,0 @@
import { applyMatrix, grayscale, type Filter, clone, add } from './processor';
export type FilterFn = (imageData: ImageData) => void;
export function boxFilter(amount = 2): FilterFn {
return (imageData: ImageData) => {
const size = amount * 2 + 1;
const matrix = [];
for (let i = 0; i < size; i++) {
const row = [];
for (let j = 0; j < size; j++) {
row.push(1);
}
matrix.push(row);
}
applyMatrix(imageData, {
div: size ** 2,
matrix,
});
};
}
export const sharpening: FilterFn = (imageData: ImageData) => {
applyMatrix(imageData, {
matrix: [
[0, -1, 0],
[-1, 5, -1],
[0, -1, 0],
],
});
};
export const edgeDetection: FilterFn = (imageData: ImageData) => {
grayscale(imageData);
applyMatrix(imageData, {
matrix: [
[0, -1, 0],
[-1, 4, -1],
[0, -1, 0],
],
});
};
export const sobel: FilterFn = (imageData: ImageData) => {
grayscale(imageData);
const data2 = clone(imageData);
applyMatrix(imageData, {
matrix: [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1],
],
});
applyMatrix(data2, {
matrix: [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1],
],
});
add(imageData, data2);
};

View File

@ -1,209 +0,0 @@
import type { FilterFn } from './filters';
export type Filter = {
matrix: number[][];
div?: number;
};
export async function applyFilter(
src: string,
filter: FilterFn,
): Promise<string> {
const image = await loadImage(src);
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
filter(imageData);
ctx.putImageData(imageData, 0, 0);
const url = canvas.toDataURL();
canvas.remove();
return url;
}
async function loadImage(src: string) {
const image = new Image();
image.src = src;
return new Promise<HTMLImageElement>((res, rej) => {
image.onload = () => res(image);
image.onerror = rej;
});
}
async function getImageData(src: string): Promise<ImageData> {
const image = await loadImage(src);
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
return ctx.getImageData(0, 0, canvas.width, canvas.height);
}
export async function getHistogram(src: string) {
const imageData = await getImageData(src);
const data = new ImageDataProxy(imageData);
const red = new Array(256).fill(0);
const green = new Array(256).fill(0);
const blue = new Array(256).fill(0);
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const pix = data.getPixel(x, y);
red[pix.r]++;
green[pix.g]++;
blue[pix.b]++;
}
}
return { red, green, blue };
}
export function grayscale(imageData: ImageData) {
const data = new ImageDataProxy(imageData);
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const pix = data.getPixel(x, y);
const avg = pix.r * 0.299 + pix.g * 0.587 + pix.b * 0.114;
data.setPixel(x, y, { r: avg, g: avg, b: avg, a: pix.a });
}
}
}
export function add(imageData1: ImageData, imageData2: ImageData) {
const data1 = new ImageDataProxy(imageData1);
const data2 = new ImageDataProxy(imageData2);
for (let y = 0; y < imageData1.height; y++) {
for (let x = 0; x < imageData1.width; x++) {
const pix1 = data1.getPixel(x, y);
const pix2 = data2.getPixel(x, y);
data1.setPixel(x, y, {
r: pix1.r + pix2.r,
g: pix1.g + pix2.g,
b: pix1.b + pix2.b,
a: pix1.a,
});
}
}
}
export function subtract(imageData1: ImageData, imageData2: ImageData) {
const data1 = new ImageDataProxy(imageData1);
const data2 = new ImageDataProxy(imageData2);
for (let y = 0; y < imageData1.height; y++) {
for (let x = 0; x < imageData1.width; x++) {
const pix1 = data1.getPixel(x, y);
const pix2 = data2.getPixel(x, y);
data1.setPixel(x, y, {
r: pix1.r - pix2.r,
g: pix1.g - pix2.g,
b: pix1.b - pix2.b,
a: pix1.a,
});
}
}
}
export function clone(imageData: ImageData): ImageData {
return {
width: imageData.width,
height: imageData.height,
colorSpace: imageData.colorSpace,
data: new Uint8ClampedArray(imageData.data),
};
}
export function applyMatrix(imageData: ImageData, filter: Filter) {
const { matrix, div = 1 } = filter;
const data = new ImageDataProxy(imageData);
const offset = -(matrix.length - 1) / 2;
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const pix = data.getPixel(x, y);
let sum: Pixel = { r: 0, g: 0, b: 0, a: pix.a };
for (let my = 0; my < matrix.length; my++) {
for (let mx = 0; mx < matrix[my].length; mx++) {
const mpix = data.getPixel(x + mx - offset, y + my - offset);
const val = matrix[my][mx];
sum.r += mpix.r * val;
sum.g += mpix.g * val;
sum.b += mpix.b * val;
}
}
sum.r /= div;
sum.g /= div;
sum.b /= div;
data.setPixel(x, y, sum);
}
}
}
type Pixel = {
r: number;
g: number;
b: number;
a: number;
};
export class ImageDataProxy {
constructor(private readonly imageData: ImageData) {}
public getPixel(x: number, y: number): Pixel {
const { data, width, height } = this.imageData;
if (x > width - 1) x = width - 1;
else if (x < 0) x = 0;
if (y > height - 1) y = height - 1;
else if (y < 0) y = 0;
const i = (y * width + x) * 4;
return {
r: data[i],
g: data[i + 1],
b: data[i + 2],
a: data[i + 3],
};
}
public setPixel(x: number, y: number, pixel: Pixel) {
const { data, width, height } = this.imageData;
if (x > width - 1 || y > height - 1) return;
if (pixel.r > 255) pixel.r = 255;
else if (pixel.r < 0) pixel.r = 0;
if (pixel.g > 255) pixel.g = 255;
else if (pixel.g < 0) pixel.g = 0;
if (pixel.b > 255) pixel.b = 255;
else if (pixel.b < 0) pixel.b = 0;
const i = (y * width + x) * 4;
data[i] = pixel.r;
data[i + 1] = pixel.g;
data[i + 2] = pixel.b;
data[i + 3] = pixel.a;
}
}