From 4732494021613b082f2f11afb8976983430f0b7a Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Sat, 22 Jan 2022 21:54:38 +0100 Subject: [PATCH] [orx-fx] Add Canny edge detector --- orx-fx/README.md | 6 + orx-fx/build.gradle.kts | 2 - .../kotlin/edges/CannyEdgeDetector.kt | 48 ++++++++ .../demo/kotlin/DemoCannyEdgeDetector01.kt | 16 +++ .../glsl/edges/canny-edge-detector.frag | 109 ++++++++++++++++++ 5 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 orx-fx/src/commonMain/kotlin/edges/CannyEdgeDetector.kt create mode 100644 orx-fx/src/demo/kotlin/DemoCannyEdgeDetector01.kt create mode 100644 orx-fx/src/shaders/glsl/edges/canny-edge-detector.frag diff --git a/orx-fx/README.md b/orx-fx/README.md index 19cd5bc7..1189cb68 100644 --- a/orx-fx/README.md +++ b/orx-fx/README.md @@ -86,6 +86,7 @@ All distortion effects are opacity preserving ### Edges - `LumaSobel` - A Sobel-kernel based luminosity edge detector - `EdgesWork` - An edges filter doubling as erosion + - `Contour` - detects multi-level contours ### Grain - `FilmGrain` - adds film-like grain to the source input @@ -106,6 +107,11 @@ All distortion effects are opacity preserving ![DemoFluidDistort01Kt](https://github.com/openrndr/orx/blob/media/orx-fx/images/DemoFluidDistort01Kt.png ## Demos +### DemoBlur01 +[source code](src/demo/kotlin/DemoBlur01.kt) + +![DemoBlur01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-fx/images/DemoBlur01Kt.png) + ### DemoFluidDistort01 [source code](src/demo/kotlin/DemoFluidDistort01.kt) diff --git a/orx-fx/build.gradle.kts b/orx-fx/build.gradle.kts index 89e6f9c2..84de5d30 100644 --- a/orx-fx/build.gradle.kts +++ b/orx-fx/build.gradle.kts @@ -42,8 +42,6 @@ kotlin { } } collectScreenshots { - ignore.set(listOf("DemoBlur01")) - } } } diff --git a/orx-fx/src/commonMain/kotlin/edges/CannyEdgeDetector.kt b/orx-fx/src/commonMain/kotlin/edges/CannyEdgeDetector.kt new file mode 100644 index 00000000..503c59ee --- /dev/null +++ b/orx-fx/src/commonMain/kotlin/edges/CannyEdgeDetector.kt @@ -0,0 +1,48 @@ +package org.openrndr.extra.fx.edges + +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.Filter +import org.openrndr.draw.filterShaderFromCode +import org.openrndr.extra.fx.fx_canny_edge_detector +import org.openrndr.extra.parameters.ColorParameter +import org.openrndr.extra.parameters.Description +import org.openrndr.extra.parameters.DoubleParameter + +@Description("Canny Edge Detector") +class CannyEdgeDetector : Filter( + filterShaderFromCode(fx_canny_edge_detector, "canny-edge-detector") +) { + + @DoubleParameter("threshold 0", 0.0, 100.0, order = 0) + var threshold0: Double by parameters + + @DoubleParameter("threshold 1", 0.0, 100.0, order = 1) + var threshold1: Double by parameters + + @DoubleParameter("thickness", 0.0, 10.0, order = 2) + var thickness: Double by parameters + + @ColorParameter("foreground color", order = 3) + var foregroundColor: ColorRGBa by parameters + + @DoubleParameter("foreground opacity", 0.0, 1.0, order = 4) + var foregroundOpacity: Double by parameters + + @ColorParameter("background color", order = 5) + var backgroundColor: ColorRGBa by parameters + + @DoubleParameter("background opacity", 0.0, 1.0, order = 6) + var backgroundOpacity: Double by parameters + + + init { + threshold0 = 2.0 + threshold1 = 0.0 + thickness = 1.0 + foregroundColor = ColorRGBa.WHITE + backgroundColor = ColorRGBa.BLACK + backgroundOpacity = 1.0 + foregroundOpacity = 1.0 + } + +} diff --git a/orx-fx/src/demo/kotlin/DemoCannyEdgeDetector01.kt b/orx-fx/src/demo/kotlin/DemoCannyEdgeDetector01.kt new file mode 100644 index 00000000..58dc548f --- /dev/null +++ b/orx-fx/src/demo/kotlin/DemoCannyEdgeDetector01.kt @@ -0,0 +1,16 @@ +import org.openrndr.application +import org.openrndr.draw.createEquivalent +import org.openrndr.draw.loadImage +import org.openrndr.extra.fx.edges.CannyEdgeDetector + +fun main() = application { + program { + val image = loadImage("demo-data/images/image-001.png") + val ced = CannyEdgeDetector() + val edges = image.createEquivalent() + extend { + ced.apply(image, edges) + drawer.image(edges) + } + } +} \ No newline at end of file diff --git a/orx-fx/src/shaders/glsl/edges/canny-edge-detector.frag b/orx-fx/src/shaders/glsl/edges/canny-edge-detector.frag new file mode 100644 index 00000000..f1a0552a --- /dev/null +++ b/orx-fx/src/shaders/glsl/edges/canny-edge-detector.frag @@ -0,0 +1,109 @@ +// https://www.shadertoy.com/view/sdcSz2 +// ref: (in japanese) +// https://imagingsolution.net/imaging/canny-edge-detector/ +uniform float thickness; +uniform sampler2D tex0; + +in vec2 v_texCoord0; +out vec4 o_output; + +uniform float threshold0; +uniform float threshold1; + +uniform vec4 backgroundColor; +uniform vec4 foregroundColor; +uniform float backgroundOpacity; +uniform float foregroundOpacity; + +vec2 iResolution = textureSize(tex0, 0); +vec2 fragCoord = v_texCoord0 * iResolution; + +float getAve(vec2 uv){ + vec3 rgb = texture(tex0, uv).rgb; + vec3 lum = vec3(0.299, 0.587, 0.114); + return dot(lum, rgb); +} + +// Detect edge. +vec4 sobel(vec2 fragCoord, vec2 dir){ + vec2 uv = fragCoord/iResolution.xy; + vec2 texel = 1./iResolution.xy; + float np = getAve(uv + (vec2(-1,+1) + dir ) * texel * thickness); + float zp = getAve(uv + (vec2( 0,+1) + dir ) * texel * thickness); + float pp = getAve(uv + (vec2(+1,+1) + dir ) * texel * thickness); + + float nz = getAve(uv + (vec2(-1, 0) + dir ) * texel * thickness); + // zz = 0 + float pz = getAve(uv + (vec2(+1, 0) + dir ) * texel * thickness); + + float nn = getAve(uv + (vec2(-1,-1) + dir ) * texel * thickness); + float zn = getAve(uv + (vec2( 0,-1) + dir ) * texel * thickness); + float pn = getAve(uv + (vec2(+1,-1) + dir ) * texel * thickness); + + // np zp pp + // nz zz pz + // nn zn pn + + #if 0 + float gx = (np*-1. + nz*-2. + nn*-1. + pp*1. + pz*2. + pn*1.); + float gy = (np*-1. + zp*-2. + pp*-1. + nn*1. + zn*2. + pn*1.); + #else + // https://www.shadertoy.com/view/Wds3Rl + float gx = (np*-3. + nz*-10. + nn*-3. + pp*3. + pz*10. + pn*3.); + float gy = (np*-3. + zp*-10. + pp*-3. + nn*3. + zn*10. + pn*3.); + #endif + + vec2 G = vec2(gx,gy); + + float grad = length(G); + + float angle = atan(G.y, G.x); + + return vec4(G, grad, angle); +} + +// Make edge thinner. +vec2 hysteresisThr(vec2 fragCoord, float mn, float mx){ + + vec4 edge = sobel(fragCoord, vec2(0)); + + vec2 dir = vec2(cos(edge.w), sin(edge.w)); + dir *= vec2(-1,1); // rotate 90 degrees. + + vec4 edgep = sobel(fragCoord, dir); + vec4 edgen = sobel(fragCoord, -dir); + + if(edge.z < edgep.z || edge.z < edgen.z ) edge.z = 0.; + + return vec2( + (edge.z > mn) ? edge.z : 0., + (edge.z > mx) ? edge.z : 0. + ); +} + +float cannyEdge(vec2 fragCoord, float mn, float mx){ + + vec2 np = hysteresisThr(fragCoord + vec2(-1,+1), mn, mx); + vec2 zp = hysteresisThr(fragCoord + vec2( 0,+1), mn, mx); + vec2 pp = hysteresisThr(fragCoord + vec2(+1,+1), mn, mx); + + vec2 nz = hysteresisThr(fragCoord + vec2(-1, 0), mn, mx); + vec2 zz = hysteresisThr(fragCoord + vec2( 0, 0), mn, mx); + vec2 pz = hysteresisThr(fragCoord + vec2(+1, 0), mn, mx); + + vec2 nn = hysteresisThr(fragCoord + vec2(-1,-1), mn, mx); + vec2 zn = hysteresisThr(fragCoord + vec2( 0,-1), mn, mx); + vec2 pn = hysteresisThr(fragCoord + vec2(+1,-1), mn, mx); + + // np zp pp + // nz zz pz + // nn zn pn + //return min(1., step(1e-3, zz.x) * (zp.y + nz.y + pz.y + zn.y)*8.); + //return min(1., step(1e-3, zz.x) * (np.y + pp.y + nn.y + pn.y)*8.); + return min(1., step(1e-2, zz.x*8.) * smoothstep(.0, .3, np.y + zp.y + pp.y + nz.y + pz.y + nn.y + zn.y + pn.y)*8.); +} + +void main(){ + float edge = cannyEdge(fragCoord, threshold0, threshold1); + o_output = mix(foregroundColor * foregroundOpacity, backgroundColor * backgroundOpacity, 1.-edge); +} \ No newline at end of file