diff --git a/index.html b/index.html index b502e5e..c22f54f 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,13 @@ - + imgproc + - +
- diff --git a/package.json b/package.json index fc64cae..6cf9280 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 709e1b5..7209161 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/App.svelte b/src/App.svelte index 2f4b11d..5a7640c 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,84 +1,189 @@ -
- - - - - - - - - -
- - - {#if processing} -
- -
- {:else if filterSrc} - - {/if} +
+
+
-
- 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} +

Filters

+
+ + + + + + + + + +
+ +
+ + +
+
+
+ +

{thresholdValue}

+ +
+
+ + + + + +
+
+ + + + + +
+
+ +

{brightnessValue}

+ +
+ {/if} + +
+
+
+ + {#if processing} +
+ +
+ {/if} +
+
+ {#each filters as filter} +

{filter.name}

+ {/each} +
diff --git a/src/assets/banana.png b/src/assets/banana.png new file mode 100644 index 0000000..292a77d Binary files /dev/null and b/src/assets/banana.png differ diff --git a/src/assets/gapi.png b/src/assets/gapi.png deleted file mode 100644 index 6141d81..0000000 Binary files a/src/assets/gapi.png and /dev/null differ diff --git a/src/assets/image.jpeg b/src/assets/image.jpeg new file mode 100644 index 0000000..bc11f7f Binary files /dev/null and b/src/assets/image.jpeg differ diff --git a/src/assets/mountains.avif b/src/assets/mountains.avif deleted file mode 100644 index a268ea3..0000000 Binary files a/src/assets/mountains.avif and /dev/null differ diff --git a/src/lib/components/Histogram.svelte b/src/lib/components/Histogram.svelte new file mode 100644 index 0000000..cd00aaa --- /dev/null +++ b/src/lib/components/Histogram.svelte @@ -0,0 +1,40 @@ + + + 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, + }, + ], + }} +/> diff --git a/src/lib/components/ImageUpload.svelte b/src/lib/components/ImageUpload.svelte new file mode 100644 index 0000000..0a9ba8b --- /dev/null +++ b/src/lib/components/ImageUpload.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/engine/filters.ts b/src/lib/engine/filters.ts new file mode 100644 index 0000000..9b1f8c2 --- /dev/null +++ b/src/lib/engine/filters.ts @@ -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; + +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); diff --git a/src/lib/engine/processor.ts b/src/lib/engine/processor.ts new file mode 100644 index 0000000..d81694a --- /dev/null +++ b/src/lib/engine/processor.ts @@ -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 { + 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( + './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((res, rej) => { + image.onload = () => res(image); + image.onerror = rej; + }); +} + +async function getImageData(src: string): Promise { + 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( + './workers/getHistogram.ts', + imageData, + ); + return histogram; +} + +async function runInWorker(url: string, data: I) { + return new Promise((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) => res(e.data); + worker.postMessage(data); + }); +} + +export async function grayscale(imageData: ImageData) { + const newImageData = await runInWorker( + './workers/grayscale.ts', + imageData, + ); + imageData.data.set(newImageData.data); +} + +export async function add(imageData1: ImageData, imageData2: ImageData) { + const res = await runInWorker('./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( + './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( + './workers/treshold.ts', + { + imageData, + threshold, + }, + ); + imageData.data.set(newImageData.data); +} + +export async function removeColorChannel( + imageData: ImageData, + colorChannel: ColorChannel, +) { + const newImageData = await runInWorker( + './workers/removeColorChannel.ts', + { + imageData, + colorChannel, + }, + ); + imageData.data.set(newImageData.data); +} + +export async function enhanceColorChannel( + imageData: ImageData, + colorChannel: ColorChannel, +) { + const newImageData = await runInWorker( + './workers/enhanceColorChannel.ts', + { + imageData, + colorChannel, + }, + ); + imageData.data.set(newImageData.data); +} + +export async function setBrightness(imageData: ImageData, brightness: number) { + const newImageData = await runInWorker( + './workers/setBrightness.ts', + { + imageData, + brightness, + }, + ); + imageData.data.set(newImageData.data); +} diff --git a/src/lib/engine/proxy.ts b/src/lib/engine/proxy.ts new file mode 100644 index 0000000..9a1f337 --- /dev/null +++ b/src/lib/engine/proxy.ts @@ -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; + } +} diff --git a/src/lib/engine/types.ts b/src/lib/engine/types.ts new file mode 100644 index 0000000..be0b7d0 --- /dev/null +++ b/src/lib/engine/types.ts @@ -0,0 +1,5 @@ +export enum ColorChannel { + red = 0, + green = 1, + blue = 2, +} diff --git a/src/lib/engine/worker.ts b/src/lib/engine/worker.ts new file mode 100644 index 0000000..08fb6af --- /dev/null +++ b/src/lib/engine/worker.ts @@ -0,0 +1,8 @@ +type WorkerFn = (data: any) => T; + +export function registerWorker(func: WorkerFn) { + self.onmessage = (e: MessageEvent) => { + const result = func(e.data); + self.postMessage(result); + }; +} diff --git a/src/lib/engine/workers/add.ts b/src/lib/engine/workers/add.ts new file mode 100644 index 0000000..147331e --- /dev/null +++ b/src/lib/engine/workers/add.ts @@ -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 }; +}); diff --git a/src/lib/engine/workers/applyMatrix.ts b/src/lib/engine/workers/applyMatrix.ts new file mode 100644 index 0000000..af7bebd --- /dev/null +++ b/src/lib/engine/workers/applyMatrix.ts @@ -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 {}; diff --git a/src/lib/engine/workers/enhanceColorChannel.ts b/src/lib/engine/workers/enhanceColorChannel.ts new file mode 100644 index 0000000..4f0f5ca --- /dev/null +++ b/src/lib/engine/workers/enhanceColorChannel.ts @@ -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 {}; diff --git a/src/lib/engine/workers/getHistogram.ts b/src/lib/engine/workers/getHistogram.ts new file mode 100644 index 0000000..c0f896d --- /dev/null +++ b/src/lib/engine/workers/getHistogram.ts @@ -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 {}; diff --git a/src/lib/engine/workers/grayscale.ts b/src/lib/engine/workers/grayscale.ts new file mode 100644 index 0000000..46cfb3a --- /dev/null +++ b/src/lib/engine/workers/grayscale.ts @@ -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 {}; diff --git a/src/lib/engine/workers/removeColorChannel.ts b/src/lib/engine/workers/removeColorChannel.ts new file mode 100644 index 0000000..22d7b50 --- /dev/null +++ b/src/lib/engine/workers/removeColorChannel.ts @@ -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 {}; diff --git a/src/lib/engine/workers/setBrightness.ts b/src/lib/engine/workers/setBrightness.ts new file mode 100644 index 0000000..b6d2a2f --- /dev/null +++ b/src/lib/engine/workers/setBrightness.ts @@ -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 {}; diff --git a/src/lib/engine/workers/subtract.ts b/src/lib/engine/workers/subtract.ts new file mode 100644 index 0000000..42ee8fe --- /dev/null +++ b/src/lib/engine/workers/subtract.ts @@ -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 }; +}); diff --git a/src/lib/engine/workers/treshold.ts b/src/lib/engine/workers/treshold.ts new file mode 100644 index 0000000..9ec3b8e --- /dev/null +++ b/src/lib/engine/workers/treshold.ts @@ -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 {}; diff --git a/src/lib/filters.ts b/src/lib/filters.ts deleted file mode 100644 index 7a70d52..0000000 --- a/src/lib/filters.ts +++ /dev/null @@ -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); -}; diff --git a/src/lib/processor.ts b/src/lib/processor.ts deleted file mode 100644 index 3d71739..0000000 --- a/src/lib/processor.ts +++ /dev/null @@ -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 { - 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((res, rej) => { - image.onload = () => res(image); - image.onerror = rej; - }); -} - -async function getImageData(src: string): Promise { - 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; - } -}