diff --git a/src/nodes/WebGL/Levels.ts b/src/nodes/WebGL/Levels.ts new file mode 100644 index 0000000..3e19105 --- /dev/null +++ b/src/nodes/WebGL/Levels.ts @@ -0,0 +1,113 @@ +import WebGL from './WebGl'; +import { + ImageVar, + NumberVar, +} from '../io/AbstractIOSet'; +import {canvasPool2D} from "graphfx/src/canvas/CanvasPool"; +import {Variables} from "graphfx/src/nodes/io/AbstractIOSet"; +import {mediaSize} from "../canvas"; + +const inputs = { + image: { + type: 'Image' + } as ImageVar, + shadow: { + type: 'Number', + min: 0, + max: 255, + default: 0 + } as NumberVar, + gamma: { + type: 'Number', + min: 0.1, + max: 9.9, + default: 1, + step: 0.1, + } as NumberVar, + highlight: { + type: 'Number', + min: 0, + max: 255, + default: 255 + } as NumberVar, + dark: { + type: 'Number', + min: 0, + max: 255, + default: 0 + } as NumberVar, + light: { + type: 'Number', + min: 0, + max: 255, + default: 255 + } as NumberVar, +} + +export abstract class LevelsAbstract extends WebGL { + protected constructor(name: string, inputs: INPUTS) { + super(name, inputs); + } + + get frag() { + return ` + precision mediump float; + + uniform sampler2D u_image; + uniform vec2 u_resolution; + uniform float u_shadow; + uniform float u_highlight; + uniform float u_gamma; + uniform float u_dark; + uniform float u_light; + + void main() { + vec2 uv = gl_FragCoord.xy / u_resolution; + uv.y = 1.0 - uv.y; // flip the y-coordinate + vec4 color = texture2D(u_image, uv); + color.rgb = (color.rgb - vec3(u_shadow / 255.0)) * (1.0 / ((u_highlight - u_shadow) / 255.0)); + color.rgb = (color.rgb - 0.5) * u_gamma + 0.5; + color.rgb = color.rgb * ((u_light - u_dark) / 255.0) + vec3(u_dark / 255.0); + color.rgb = clamp(color.rgb, 0.0, 1.0); + gl_FragColor = color; + } + `; + } + + _setParams(gl: WebGLRenderingContext, program: WebGLProgram) { + const image = this.in.image.value; + // @ts-ignore + const {width, height} = mediaSize(image) + const canvas = canvasPool2D.createCanvas(); + canvas.width = width; + canvas.height = height; + const context = canvas.getContext('2d'); + context.drawImage(image as CanvasImageSource | OffscreenCanvas, 0, 0); + this._setParamsWithCanvas(gl, program, canvas); + } + + _setParamsWithCanvas(gl: WebGLRenderingContext, program: WebGLProgram, canvas: OffscreenCanvas | HTMLCanvasElement) { + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + + gl.uniform1i(gl.getUniformLocation(program, 'u_image'), 0); + gl.uniform2f(gl.getUniformLocation(program, 'u_resolution'), canvas.width, canvas.height); + } +} + +export default class Levels extends LevelsAbstract { + + constructor() { + super('Levels', inputs) + } + + _setParamsWithCanvas(gl: WebGLRenderingContext, program: WebGLProgram, canvas: OffscreenCanvas | HTMLCanvasElement) { + super._setParamsWithCanvas(gl, program, canvas); + gl.uniform1f(gl.getUniformLocation(program, 'u_shadow'), this.in.shadow.value); + gl.uniform1f(gl.getUniformLocation(program, 'u_highlight'), this.in.highlight.value); + gl.uniform1f(gl.getUniformLocation(program, 'u_gamma'), this.in.gamma.value); + gl.uniform1f(gl.getUniformLocation(program, 'u_dark'), this.in.dark.value); + gl.uniform1f(gl.getUniformLocation(program, 'u_light'), this.in.light.value); + } +} \ No newline at end of file diff --git a/src/nodes/WebGL/LevelsAuto.ts b/src/nodes/WebGL/LevelsAuto.ts new file mode 100644 index 0000000..0724fcf --- /dev/null +++ b/src/nodes/WebGL/LevelsAuto.ts @@ -0,0 +1,85 @@ +import {LevelsAbstract} from './Levels'; +import { + ImageVar, +} from '../io/AbstractIOSet'; + +const inputs = { + image: { + type: 'Image' + } as ImageVar, +} + +export default class LevelsAuto extends LevelsAbstract { + + constructor() { + super('LevelsAuto', inputs) + } + + _setParamsWithCanvas(gl: WebGLRenderingContext, program: WebGLProgram, canvas: HTMLCanvasElement) { + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height).data; + const histogram = this.createHistogram(imageData); + const threshold = this.calculateThreshold(histogram, imageData); + + // Find shadow and highlight + let shadow = histogram.findIndex(value => value > threshold); + let highlight = 255 - histogram.slice().reverse().findIndex(value => value > threshold); + + super._setParamsWithCanvas(gl, program, canvas); + gl.uniform1f(gl.getUniformLocation(program, 'u_shadow'), shadow); + gl.uniform1f(gl.getUniformLocation(program, 'u_highlight'), highlight); + gl.uniform1f(gl.getUniformLocation(program, 'u_gamma'), 1); + gl.uniform1f(gl.getUniformLocation(program, 'u_dark'), 0); + gl.uniform1f(gl.getUniformLocation(program, 'u_light'), 255); + } + + private createHistogram(imageData: Uint8ClampedArray): number[] { + const histogram = new Array(256).fill(0); + for (let i = 0; i < imageData.length; i += 4) { + if (imageData[i + 3] > 0) { // Check if the pixel is not transparent + let brightness = Math.round(0.299 * imageData[i] + 0.587 * imageData[i + 1] + 0.114 * imageData[i + 2]); + histogram[brightness]++; + } + } + + return histogram; + } + + private calculateThreshold(histogram: number[], imageData: Uint8ClampedArray) { + let total = imageData.length / 4; // total number of pixels + let sum = 0; + for (let i = 0; i < 256; i++) { + sum += i * histogram[i]; + } + + let sumB = 0; + let wB = 0; + let wF = 0; + let varMax = 0; + let threshold = 0; + + for (let i = 0; i < 256; i++) { + wB += histogram[i]; // Weight Background + if (wB === 0) continue; + + wF = total - wB; // Weight Foreground + if (wF === 0) break; + + sumB += i * histogram[i]; + + let mB = sumB / wB; // Mean Background + let mF = (sum - sumB) / wF; // Mean Foreground + + // Calculate Between Class Variance + let varBetween = wB * wF * (mB - mF) * (mB - mF); + + // Check if new maximum found + if (varBetween > varMax) { + varMax = varBetween; + threshold = i; + } + } + + return threshold; + } +} \ No newline at end of file diff --git a/src/nodes/index.ts b/src/nodes/index.ts index 3fac825..38b7afb 100644 --- a/src/nodes/index.ts +++ b/src/nodes/index.ts @@ -20,6 +20,8 @@ export {default as Blur} from './WebGL/Blur'; export {default as SoftEdge} from './WebGL/SoftEdge'; export {default as Edge} from './WebGL/Edge'; export {default as KernelPreset} from './WebGL/KernelPreset'; +export {default as Levels} from './WebGL/Levels'; +export {default as LevelsAuto} from './WebGL/LevelsAuto'; // Math export {default as Numbers} from './Math/Numbers'; export {default as NumberBinaryOperation} from './Math/NumberBinaryOperation';