From 5038fd0fc32ea648cbaf332022e7d16b5d39f8aa Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Thu, 2 Feb 2023 18:24:07 +0100 Subject: [PATCH] [orx-marching-squares] Add orx-marching-squares module --- build.gradle | 1 + orx-marching-squares/README.md | 21 ++++ orx-marching-squares/build.gradle.kts | 23 ++++ .../src/commonMain/kotlin/MarchingSquares.kt | 107 ++++++++++++++++++ .../src/jvmDemo/kotlin/FindContours01.kt | 22 ++++ .../src/jvmDemo/kotlin/FindContours02.kt | 24 ++++ .../src/jvmDemo/kotlin/FindContours03.kt | 30 +++++ .../src/jvmDemo/kotlin/FindContours04.kt | 36 ++++++ settings.gradle.kts | 1 + 9 files changed, 265 insertions(+) create mode 100644 orx-marching-squares/README.md create mode 100644 orx-marching-squares/build.gradle.kts create mode 100644 orx-marching-squares/src/commonMain/kotlin/MarchingSquares.kt create mode 100644 orx-marching-squares/src/jvmDemo/kotlin/FindContours01.kt create mode 100644 orx-marching-squares/src/jvmDemo/kotlin/FindContours02.kt create mode 100644 orx-marching-squares/src/jvmDemo/kotlin/FindContours03.kt create mode 100644 orx-marching-squares/src/jvmDemo/kotlin/FindContours04.kt diff --git a/build.gradle b/build.gradle index b5584c90..5f787875 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ def multiplatformModules = [ "orx-gradient-descent", "orx-image-fit", "orx-jumpflood", + "orx-marching-squares", "orx-no-clear", "orx-noise", "orx-parameters", diff --git a/orx-marching-squares/README.md b/orx-marching-squares/README.md new file mode 100644 index 00000000..a78e0baa --- /dev/null +++ b/orx-marching-squares/README.md @@ -0,0 +1,21 @@ +# orx-marching-squares + +Tools for extracting contours from functions + +## How to use it? + +`orx-marching-squares` provides the `findContours()` function + +```kotlin +fun f(v: Vector2) = v.distanceTo(drawer.bounds.center) - 200.0 +val segments = findContours(::f, drawer.bounds, 16.0) +drawer.lineSegments(segments) +``` + +With a small adjustment to the given function one can use `findContours` to find iso contours. The trick is to add a cosine over the distance function. + +```kotlin +fun f(v: Vector2) = cos((v.distanceTo(drawer.bounds.center) / 100.0) * 2 * PI) +val segments = findContours(::f, drawer.bounds.offsetEdges(32.0), 16.0) +drawer.lineSegments(segments) +``` \ No newline at end of file diff --git a/orx-marching-squares/build.gradle.kts b/orx-marching-squares/build.gradle.kts new file mode 100644 index 00000000..327da5ab --- /dev/null +++ b/orx-marching-squares/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + org.openrndr.extra.convention.`kotlin-multiplatform` +} + +kotlin { + sourceSets { + @Suppress("UNUSED_VARIABLE") + val commonMain by getting { + dependencies { + api(libs.openrndr.math) + api(libs.openrndr.shape) + } + } + + @Suppress("UNUSED_VARIABLE") + val jvmDemo by getting { + dependencies { + implementation(project(":orx-shapes")) + implementation(project(":orx-noise")) + } + } + } +} \ No newline at end of file diff --git a/orx-marching-squares/src/commonMain/kotlin/MarchingSquares.kt b/orx-marching-squares/src/commonMain/kotlin/MarchingSquares.kt new file mode 100644 index 00000000..31982b11 --- /dev/null +++ b/orx-marching-squares/src/commonMain/kotlin/MarchingSquares.kt @@ -0,0 +1,107 @@ +package org.openrndr.extra.marchingsquares + +import org.openrndr.math.IntVector2 +import org.openrndr.math.Vector2 +import org.openrndr.shape.LineSegment +import org.openrndr.shape.Rectangle +import kotlin.math.max +import kotlin.math.min + +fun findContours( + f: (Vector2) -> Double, + area: Rectangle, + cellSize: Double, + useInterpolation: Boolean = true +): List { + val segments = mutableListOf() + val values = mutableMapOf() + + for (y in 0 until (area.width / cellSize).toInt()) { + for (x in 0 until (area.width / cellSize).toInt()) { + values[IntVector2(x, y)] = f(Vector2(x * cellSize + area.x, y * cellSize + area.y)) + } + } + + val zero = 0.0 + for (y in 0 until (area.width / cellSize).toInt()) { + for (x in 0 until (area.width / cellSize).toInt()) { + val v00 = (values[IntVector2(x, y)] ?: zero) + val v10 = (values[IntVector2(x + 1, y)] ?: zero) + val v01 = (values[IntVector2(x, y + 1)] ?: zero) + val v11 = (values[IntVector2(x + 1, y + 1)] ?: zero) + + val p00 = Vector2(x.toDouble(), y.toDouble()) * cellSize + area.corner + val p10 = Vector2((x + 1).toDouble(), y.toDouble()) * cellSize + area.corner + val p01 = Vector2(x.toDouble(), (y + 1).toDouble()) * cellSize + area.corner + val p11 = Vector2((x + 1).toDouble(), (y + 1).toDouble()) * cellSize + area.corner + + val index = (if (v00 >= 0.0) 1 else 0) + + (if (v10 >= 0.0) 2 else 0) + + (if (v01 >= 0.0) 4 else 0) + + (if (v11 >= 0.0) 8 else 0) + + fun blend(v1: Double, v2: Double): Double { + if (useInterpolation) { + require(v1 == v1 && v2 == v2) + val f1 = min(v1, v2) + val f2 = max(v1, v2) + val v = (-f1) / (f2 - f1) + + require(v == v) + require(v in 0.0..1.0) + + return if (f1 == v1) { + v + } else { + 1.0 - v + } + } else { + return 0.5 + } + } + + fun emitLine( + p00: Vector2, p01: Vector2, v00: Double, v01: Double, + p10: Vector2, p11: Vector2, v10: Double, v11: Double + ) { + val r0 = blend(v00, v01) + val r1 = blend(v10, v11) + val l0 = LineSegment(p00.mix(p01, r0), p10.mix(p11, r1)) + segments.add(l0) + } + + when (index) { + 0, 15 -> {} + 1, 15 xor 1 -> { + emitLine(p00, p01, v00, v01, p00, p10, v00, v10) + } + + 2, 15 xor 2 -> { + emitLine(p00, p10, v00, v10, p10, p11, v10, v11) + } + + 3, 15 xor 3 -> { + emitLine(p00, p01, v00, v01, p10, p11, v10, v11) + } + + 4, 15 xor 4 -> { + emitLine(p00, p01, v00, v01, p01, p11, v01, v11) + } + + 5, 15 xor 5 -> { + emitLine(p00, p10, v00, v10, p01, p11, v01, v11) + } + + 6, 15 xor 6 -> { + emitLine(p00, p01, v00, v01, p00, p10, v00, v10) + emitLine(p01, p11, v01, v11, p10, p11, v10, v11) + } + + 7, 15 xor 7 -> { + emitLine(p01, p11, v01, v11, p10, p11, v10, v11) + } + } + } + } + return segments +} \ No newline at end of file diff --git a/orx-marching-squares/src/jvmDemo/kotlin/FindContours01.kt b/orx-marching-squares/src/jvmDemo/kotlin/FindContours01.kt new file mode 100644 index 00000000..d8354edd --- /dev/null +++ b/orx-marching-squares/src/jvmDemo/kotlin/FindContours01.kt @@ -0,0 +1,22 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.marchingsquares.findContours +import org.openrndr.math.Vector2 + +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + extend { + drawer.clear(ColorRGBa.BLACK) + drawer.stroke = ColorRGBa.PINK + fun f(v: Vector2) = v.distanceTo(drawer.bounds.center) - 200.0 + val segments = findContours(::f, drawer.bounds, 16.0) + drawer.lineSegments(segments) + } + } + } +} \ No newline at end of file diff --git a/orx-marching-squares/src/jvmDemo/kotlin/FindContours02.kt b/orx-marching-squares/src/jvmDemo/kotlin/FindContours02.kt new file mode 100644 index 00000000..225d2b15 --- /dev/null +++ b/orx-marching-squares/src/jvmDemo/kotlin/FindContours02.kt @@ -0,0 +1,24 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.marchingsquares.findContours +import org.openrndr.math.Vector2 +import kotlin.math.PI +import kotlin.math.cos + +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + extend { + drawer.clear(ColorRGBa.BLACK) + drawer.stroke = ColorRGBa.PINK + fun f(v: Vector2) = cos((v.distanceTo(drawer.bounds.center) / 100.0) * 2 * PI) + val segments = findContours(::f, drawer.bounds.offsetEdges(32.0), 16.0) + drawer.lineSegments(segments) + } + } + } +} \ No newline at end of file diff --git a/orx-marching-squares/src/jvmDemo/kotlin/FindContours03.kt b/orx-marching-squares/src/jvmDemo/kotlin/FindContours03.kt new file mode 100644 index 00000000..0a8746a5 --- /dev/null +++ b/orx-marching-squares/src/jvmDemo/kotlin/FindContours03.kt @@ -0,0 +1,30 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.marchingsquares.findContours +import org.openrndr.math.Vector2 +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin + +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + extend { + drawer.clear(ColorRGBa.BLACK) + drawer.stroke = ColorRGBa.PINK + fun f(v: Vector2): Double { + + val p = v + Vector2(cos(v.y * 0.1 + seconds) * 40.0, sin(v.x * 0.1 + seconds) * 40.0) + return cos((p.distanceTo(drawer.bounds.center) / 720.0) * 6 * PI) + } + + val segments = findContours(::f, drawer.bounds.offsetEdges(32.0), 4.0) + drawer.lineSegments(segments) + } + } + } +} \ No newline at end of file diff --git a/orx-marching-squares/src/jvmDemo/kotlin/FindContours04.kt b/orx-marching-squares/src/jvmDemo/kotlin/FindContours04.kt new file mode 100644 index 00000000..5a285219 --- /dev/null +++ b/orx-marching-squares/src/jvmDemo/kotlin/FindContours04.kt @@ -0,0 +1,36 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.grayscale +import org.openrndr.draw.loadImage +import org.openrndr.extra.marchingsquares.findContours +import org.openrndr.math.Vector2 +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin + +fun main() { + application { + configure { + width = 640 + height = 480 + } + program { + val image = loadImage("demo-data/images/image-001.png") + image.shadow.download() + extend { + drawer.clear(ColorRGBa.BLACK) + drawer.stroke = ColorRGBa.BLACK + fun f(v: Vector2): Double { + val iv = v.toInt() + val d = if (iv.x >= 0 && iv.y >= 0 && iv.x < image.width && iv.y < image.height) image.shadow[iv.x, iv.y].luminance else 0.0 + return cos(d * PI * 8.0 + seconds) + } + + val segments = findContours(::f, drawer.bounds.offsetEdges(32.0), 4.0) + drawer.drawStyle.colorMatrix = grayscale() + drawer.image(image) + drawer.lineSegments(segments) + } + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index bc20bbdc..e6e4d2b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,6 +47,7 @@ include( "orx-no-clear", "orx-noise", "orx-obj-loader", + "orx-marching-squares", "orx-jvm:orx-olive", "orx-jvm:orx-osc", "orx-palette",