[orx-marching-squares] Change findContours to return List<ShapeContour>

This commit is contained in:
Edwin Jakobs
2023-02-02 22:03:51 +01:00
parent 9019e7e7ea
commit 5e6123c958
5 changed files with 65 additions and 18 deletions

View File

@@ -4,17 +4,28 @@ import org.openrndr.math.IntVector2
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.shape.LineSegment import org.openrndr.shape.LineSegment
import org.openrndr.shape.Rectangle import org.openrndr.shape.Rectangle
import org.openrndr.shape.ShapeContour
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
/**
* Find contours for a function [f] using the marching squares algorithm. A contour is found when f(x) crosses zero.
* @param f the function
* @param area a rectangular area in which the function should be evaluated
* @param cellSize the size of the cells, smaller size gives higher resolution
* @param useInterpolation intersection points will be interpolated if true, default true
* @return a list of [ShapeContour] instances
*/
fun findContours( fun findContours(
f: (Vector2) -> Double, f: (Vector2) -> Double,
area: Rectangle, area: Rectangle,
cellSize: Double, cellSize: Double,
useInterpolation: Boolean = true useInterpolation: Boolean = true
): List<LineSegment> { ): List<ShapeContour> {
val segments = mutableListOf<LineSegment>() val segments = mutableListOf<LineSegment>()
val values = mutableMapOf<IntVector2, Double>() val values = mutableMapOf<IntVector2, Double>()
val segmentsMap = mutableMapOf<Vector2, MutableList<LineSegment>>()
for (y in 0 until (area.width / cellSize).toInt()) { for (y in 0 until (area.width / cellSize).toInt()) {
for (x in 0 until (area.width / cellSize).toInt()) { for (x in 0 until (area.width / cellSize).toInt()) {
@@ -25,9 +36,12 @@ fun findContours(
val zero = 0.0 val zero = 0.0
for (y in 0 until (area.width / cellSize).toInt()) { for (y in 0 until (area.width / cellSize).toInt()) {
for (x 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) // Here we check if we are at a right or top border. This is to ensure we create closed contours
val v01 = (values[IntVector2(x, y + 1)] ?: zero) // later on in the process.
val v00 = if (x == 0 || y == 0) zero else (values[IntVector2(x, y)] ?: zero)
val v10 = if (y == 0) zero else (values[IntVector2(x + 1, y)] ?: zero)
val v01 = if (x == 0) zero else (values[IntVector2(x, y + 1)] ?: zero)
val v11 = (values[IntVector2(x + 1, y + 1)] ?: zero) val v11 = (values[IntVector2(x + 1, y + 1)] ?: zero)
val p00 = Vector2(x.toDouble(), y.toDouble()) * cellSize + area.corner val p00 = Vector2(x.toDouble(), y.toDouble()) * cellSize + area.corner
@@ -66,7 +80,12 @@ fun findContours(
) { ) {
val r0 = blend(v00, v01) val r0 = blend(v00, v01)
val r1 = blend(v10, v11) val r1 = blend(v10, v11)
val l0 = LineSegment(p00.mix(p01, r0), p10.mix(p11, r1))
val v0 = p00.mix(p01, r0)
val v1 = p10.mix(p11, r1)
val l0 = LineSegment(v0, v1)
segmentsMap.getOrPut(v1) { mutableListOf() }.add(l0)
segmentsMap.getOrPut(v0) { mutableListOf() }.add(l0)
segments.add(l0) segments.add(l0)
} }
@@ -103,5 +122,32 @@ fun findContours(
} }
} }
} }
return segments
val processedSegments = mutableSetOf<LineSegment>()
val contours = mutableListOf<ShapeContour>()
for (segment in segments) {
if (segment in processedSegments) {
continue
} else {
val collected = mutableListOf<Vector2>()
var current: LineSegment? = segment
var closed = true
do {
current!!
collected.add(current.start)
processedSegments.add(current)
if (segmentsMap[current.start]!!.size < 2) {
closed = false
}
val hold = current
current = segmentsMap[current.start]?.firstOrNull { it !in processedSegments }
if (current == null) {
current = segmentsMap[hold.end]?.firstOrNull { it !in processedSegments }
}
} while (current != segment && current != null)
contours.add(ShapeContour.fromPoints(collected, closed = closed))
}
}
return contours
} }

View File

@@ -14,8 +14,9 @@ fun main() {
drawer.clear(ColorRGBa.BLACK) drawer.clear(ColorRGBa.BLACK)
drawer.stroke = ColorRGBa.PINK drawer.stroke = ColorRGBa.PINK
fun f(v: Vector2) = v.distanceTo(drawer.bounds.center) - 200.0 fun f(v: Vector2) = v.distanceTo(drawer.bounds.center) - 200.0
val segments = findContours(::f, drawer.bounds, 16.0) val contours = findContours(::f, drawer.bounds, 16.0)
drawer.lineSegments(segments) drawer.fill = null
drawer.contours(contours)
} }
} }
} }

View File

@@ -16,8 +16,9 @@ fun main() {
drawer.clear(ColorRGBa.BLACK) drawer.clear(ColorRGBa.BLACK)
drawer.stroke = ColorRGBa.PINK drawer.stroke = ColorRGBa.PINK
fun f(v: Vector2) = cos((v.distanceTo(drawer.bounds.center) / 100.0) * 2 * PI) 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) val contours = findContours(::f, drawer.bounds.offsetEdges(-24.0), 16.0)
drawer.lineSegments(segments) drawer.fill = null
drawer.contours(contours)
} }
} }
} }

View File

@@ -16,14 +16,13 @@ fun main() {
extend { extend {
drawer.clear(ColorRGBa.BLACK) drawer.clear(ColorRGBa.BLACK)
drawer.stroke = ColorRGBa.PINK drawer.stroke = ColorRGBa.PINK
drawer.fill = null
fun f(v: Vector2): Double { 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) 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) return cos((p.distanceTo(drawer.bounds.center) / 720.0) * 12 * PI)
} }
val contours = findContours(::f, drawer.bounds.offsetEdges(-2.0), 4.0)
val segments = findContours(::f, drawer.bounds.offsetEdges(32.0), 4.0) drawer.contours(contours)
drawer.lineSegments(segments)
} }
} }
} }

View File

@@ -6,7 +6,6 @@ import org.openrndr.extra.marchingsquares.findContours
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin
fun main() { fun main() {
application { application {
@@ -20,16 +19,17 @@ fun main() {
extend { extend {
drawer.clear(ColorRGBa.BLACK) drawer.clear(ColorRGBa.BLACK)
drawer.stroke = ColorRGBa.BLACK drawer.stroke = ColorRGBa.BLACK
drawer.fill = null
fun f(v: Vector2): Double { fun f(v: Vector2): Double {
val iv = v.toInt() 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 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) return cos(d * PI * 8.0 + seconds)
} }
val segments = findContours(::f, drawer.bounds.offsetEdges(32.0), 4.0) val contours = findContours(::f, drawer.bounds.offsetEdges(32.0), 4.0)
drawer.drawStyle.colorMatrix = grayscale() drawer.drawStyle.colorMatrix = grayscale()
drawer.image(image) drawer.image(image)
drawer.lineSegments(segments) drawer.contours(contours)
} }
} }
} }