Stuff
This commit is contained in:
parent
de93ee957b
commit
2ca44089b3
|
@ -1,13 +1,13 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>imgproc</title>
|
<title>imgproc</title>
|
||||||
|
<script defer type="module" src="/src/main.ts"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-white dark:bg-gray-800">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"@popperjs/core": "^2.11.7",
|
"@popperjs/core": "^2.11.7",
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.0.3",
|
"@sveltejs/vite-plugin-svelte": "^2.0.3",
|
||||||
"@tsconfig/svelte": "^3.0.0",
|
"@tsconfig/svelte": "^3.0.0",
|
||||||
|
"@types/node": "^18.15.12",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"chart.js": "^4.2.1",
|
"chart.js": "^4.2.1",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
|
|
|
@ -10,6 +10,9 @@ devDependencies:
|
||||||
'@tsconfig/svelte':
|
'@tsconfig/svelte':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^18.15.12
|
||||||
|
version: 18.15.12
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.7
|
specifier: ^10.4.7
|
||||||
version: 10.4.7(postcss@8.4.21)
|
version: 10.4.7(postcss@8.4.21)
|
||||||
|
@ -60,7 +63,7 @@ devDependencies:
|
||||||
version: 4.9.3
|
version: 4.9.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0(@types/node@18.15.12)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
@ -420,7 +423,7 @@ packages:
|
||||||
magic-string: 0.29.0
|
magic-string: 0.29.0
|
||||||
svelte: 3.55.1
|
svelte: 3.55.1
|
||||||
svelte-hmr: 0.15.1(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)
|
vitefu: 0.2.4(vite@4.2.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -430,6 +433,10 @@ packages:
|
||||||
resolution: {integrity: sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==}
|
resolution: {integrity: sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/node@18.15.12:
|
||||||
|
resolution: {integrity: sha512-Wha1UwsB3CYdqUm2PPzh/1gujGCNtWVUYF0mB00fJFoR4gTyWTDPjSm+zBF787Ahw8vSGgBja90MkgFwvB86Dg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/pug@2.0.6:
|
/@types/pug@2.0.6:
|
||||||
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
|
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1483,7 +1490,7 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/vite@4.2.0:
|
/vite@4.2.0(@types/node@18.15.12):
|
||||||
resolution: {integrity: sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==}
|
resolution: {integrity: sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -1508,6 +1515,7 @@ packages:
|
||||||
terser:
|
terser:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@types/node': 18.15.12
|
||||||
esbuild: 0.17.15
|
esbuild: 0.17.15
|
||||||
postcss: 8.4.21
|
postcss: 8.4.21
|
||||||
resolve: 1.22.2
|
resolve: 1.22.2
|
||||||
|
@ -1524,7 +1532,7 @@ packages:
|
||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 4.2.0
|
vite: 4.2.0(@types/node@18.15.12)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/wrappy@1.0.2:
|
/wrappy@1.0.2:
|
||||||
|
|
225
src/App.svelte
225
src/App.svelte
|
@ -1,84 +1,189 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button, ButtonGroup, Fileupload, Spinner } from 'flowbite-svelte';
|
import { Button, ButtonGroup, Spinner, Range, P } from 'flowbite-svelte';
|
||||||
import { applyFilter, getHistogram } from './lib/processor';
|
import {
|
||||||
|
applyFilter,
|
||||||
|
grayscale,
|
||||||
|
threshold as threshold,
|
||||||
|
} from './lib/engine/processor';
|
||||||
import {
|
import {
|
||||||
boxFilter,
|
|
||||||
sharpening,
|
|
||||||
type FilterFn,
|
type FilterFn,
|
||||||
edgeDetection,
|
sharpeningFilter,
|
||||||
sobel,
|
laplaceFilter,
|
||||||
} from './lib/filters';
|
sobelFilter,
|
||||||
import { Line } from 'svelte-chartjs';
|
gaussianBlurFilter,
|
||||||
|
boxBlurFilter,
|
||||||
|
removeColorChannelFilter,
|
||||||
|
thresholdFilter,
|
||||||
|
enhanceColorChannelFilter,
|
||||||
|
setBrightnessFilter,
|
||||||
|
} from './lib/engine/filters';
|
||||||
import 'chart.js/auto';
|
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 | null = demoImage;
|
||||||
let originalSrc: string;
|
let src: string | null = demoImage;
|
||||||
let filterSrc: string;
|
|
||||||
let processing = false;
|
let processing = false;
|
||||||
let histogram: { red: number[]; green: number[]; blue: number[] };
|
|
||||||
|
|
||||||
$: if (files) {
|
let thresholdValue = 128;
|
||||||
originalSrc = URL.createObjectURL(files[0]);
|
let brightnessValue = 1;
|
||||||
getHistogram(originalSrc).then((h) => (histogram = h));
|
|
||||||
|
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) {
|
function handleUpload(e: CustomEvent) {
|
||||||
processing = true;
|
src = e.detail.src;
|
||||||
filterSrc = await applyFilter(originalSrc, filter);
|
originalSrc = e.detail.src;
|
||||||
processing = false;
|
}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
filters = filters.slice(0, -1);
|
||||||
|
src = filters[filters.length - 1]?.src || originalSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
filters = [];
|
||||||
|
src = originalSrc;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4 space-y-2 w-screen">
|
<div class="p-4 space-y-2 h-screen flex flex-col">
|
||||||
<Fileupload bind:files />
|
<div class="w-max">
|
||||||
|
<ImageUpload on:upload={handleUpload} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if src}
|
||||||
|
<P>Filters</P>
|
||||||
|
<div class="flex">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button on:click={() => apply(boxFilter(2))}>Box Filter</Button>
|
<Button on:click={() => apply('Box blur', boxBlurFilter)}
|
||||||
<Button on:click={() => apply(sharpening)}>Sharpening</Button>
|
>Box blur</Button
|
||||||
<Button on:click={() => apply(edgeDetection)}>Edge detection</Button>
|
>
|
||||||
<Button on:click={() => apply(sobel)}>Sobel</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>
|
</ButtonGroup>
|
||||||
|
|
||||||
<div class="flex space-x-2 w-full">
|
<div class="grow" />
|
||||||
<img src={originalSrc} alt="" class="w-1/2" />
|
|
||||||
|
|
||||||
|
<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}
|
{#if processing}
|
||||||
<div class="flex items-center justify-center w-1/2">
|
<div class="absolute">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
{:else if filterSrc}
|
|
||||||
<img src={filterSrc} alt="" class="w-1/2" />
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-64">
|
||||||
<div class="w-1/2">
|
{#each filters as filter}
|
||||||
<Line
|
<P>{filter.name}</P>
|
||||||
data={{
|
{/each}
|
||||||
labels: new Array(256).fill(0).map((_, i) => i),
|
</div>
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
BIN
src/assets/banana.png
Normal file
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
BIN
src/assets/image.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
40
src/lib/components/Histogram.svelte
Normal file
40
src/lib/components/Histogram.svelte
Normal 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
13
src/lib/components/ImageUpload.svelte
Normal file
13
src/lib/components/ImageUpload.svelte
Normal 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
101
src/lib/engine/filters.ts
Normal 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
182
src/lib/engine/processor.ts
Normal 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
49
src/lib/engine/proxy.ts
Normal 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
5
src/lib/engine/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export enum ColorChannel {
|
||||||
|
red = 0,
|
||||||
|
green = 1,
|
||||||
|
blue = 2,
|
||||||
|
}
|
8
src/lib/engine/worker.ts
Normal file
8
src/lib/engine/worker.ts
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
27
src/lib/engine/workers/add.ts
Normal file
27
src/lib/engine/workers/add.ts
Normal 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 };
|
||||||
|
});
|
48
src/lib/engine/workers/applyMatrix.ts
Normal file
48
src/lib/engine/workers/applyMatrix.ts
Normal 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 {};
|
17
src/lib/engine/workers/enhanceColorChannel.ts
Normal file
17
src/lib/engine/workers/enhanceColorChannel.ts
Normal 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 {};
|
29
src/lib/engine/workers/getHistogram.ts
Normal file
29
src/lib/engine/workers/getHistogram.ts
Normal 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 {};
|
14
src/lib/engine/workers/grayscale.ts
Normal file
14
src/lib/engine/workers/grayscale.ts
Normal 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 {};
|
17
src/lib/engine/workers/removeColorChannel.ts
Normal file
17
src/lib/engine/workers/removeColorChannel.ts
Normal 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 {};
|
17
src/lib/engine/workers/setBrightness.ts
Normal file
17
src/lib/engine/workers/setBrightness.ts
Normal 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 {};
|
27
src/lib/engine/workers/subtract.ts
Normal file
27
src/lib/engine/workers/subtract.ts
Normal 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 };
|
||||||
|
});
|
20
src/lib/engine/workers/treshold.ts
Normal file
20
src/lib/engine/workers/treshold.ts
Normal 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 {};
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user