diff --git a/demo-data/images/image-001.png b/demo-data/images/image-001.png new file mode 100644 index 00000000..06cd7a4b Binary files /dev/null and b/demo-data/images/image-001.png differ diff --git a/orx-boofcv/src/demo/kotlin/DemoContours01.kt b/orx-boofcv/src/demo/kotlin/DemoContours01.kt new file mode 100644 index 00000000..33be72b2 --- /dev/null +++ b/orx-boofcv/src/demo/kotlin/DemoContours01.kt @@ -0,0 +1,75 @@ +import boofcv.alg.filter.binary.BinaryImageOps +import boofcv.alg.filter.binary.GThresholdImageOps +import boofcv.alg.filter.binary.ThresholdImageOps +import boofcv.struct.ConnectRule +import boofcv.struct.image.GrayU8 +import org.openrndr.application +import org.openrndr.boofcv.binding.toGrayF32 +import org.openrndr.boofcv.binding.toShapeContours +import org.openrndr.color.ColorRGBa +import org.openrndr.color.rgb +import org.openrndr.draw.loadImage +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.resourceUrl +import kotlin.math.cos +import kotlin.math.sin + +fun main() { + application { + program { + + // -- this block is for automation purposes only + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + // Load an image, convert to BoofCV format using orx-boofcv + val input = loadImage("demo-data/images/image-001.png").toGrayF32() + + // BoofCV: calculate a good threshold for the loaded image + val threshold = GThresholdImageOps.computeOtsu(input, 0.0, 255.0) + + // BoofCV: use the threshold to convert the image to black and white + val binary = GrayU8(input.width, input.height) + ThresholdImageOps.threshold(input, binary, threshold.toFloat(), false) + + // BoofCV: Contract and expand the white areas to remove noise + var filtered = BinaryImageOps.erode8(binary, 1, null) + filtered = BinaryImageOps.dilate8(filtered, 1, null) + + // BoofCV: Calculate contours as vector data + val contours = BinaryImageOps.contour(filtered, ConnectRule.EIGHT, null) + + // orx-boofcv: convert vector data to OPENRNDR ShapeContours + val externalShapes = contours.toShapeContours(true, + internal = false, external = true) + val internalShapes = contours.toShapeContours(true, + internal = true, external = false) + + extend { + drawer.run { + // Zoom in and out over time + translate(bounds.center) + scale(1.5 + 0.5 * cos(seconds * 0.2)) + translate(-bounds.center) + + stroke = null + + // Draw all external shapes + fill = rgb(0.2) + contours(externalShapes) + + // Draw internal shapes one by one to set unique colors + internalShapes.forEachIndexed { i, shp -> + val shade = 0.2 + (i % 7) * 0.1 + + 0.1 * sin(i + seconds) + fill = ColorRGBa.PINK.shade(shade) + contour(shp) + } + } + } + } + } +} \ No newline at end of file diff --git a/orx-boofcv/src/main/kotlin/ContourConversion.kt b/orx-boofcv/src/main/kotlin/ContourConversion.kt index b04c1c77..89d3d185 100644 --- a/orx-boofcv/src/main/kotlin/ContourConversion.kt +++ b/orx-boofcv/src/main/kotlin/ContourConversion.kt @@ -4,19 +4,48 @@ import boofcv.alg.filter.binary.Contour import org.openrndr.shape.Shape import org.openrndr.shape.ShapeContour -fun Contour.toShape(): Shape { - val external = external.toVector2s() - val internal = internal.filter { it.size > 0 }. map { it.toVector2s() } - val contours = listOf(ShapeContour.fromPoints(external, false)) + - internal.map { - ShapeContour.fromPoints(it, false) - } +fun Contour.toShape(closed: Boolean = false, getInternal: Boolean, getExternal: Boolean): Shape { + val contours = mutableListOf() + if (getExternal) { + val externalPoints = external.toVector2s() + contours.addAll(listOf(ShapeContour.fromPoints(externalPoints, closed))) + } + if (getInternal) { + val internalCurves = internal.filter { it.size >= 3 }.map { it.toVector2s() } + contours.addAll(internalCurves.map { internalCurve -> + ShapeContour.fromPoints(internalCurve, closed) + }) + } return Shape(contours) } -fun List.toShapes(): List { - return this.filter { it.external.isNotEmpty() }.map { - it.toShape() +fun List.toShapes(closed: Boolean = false, + internal: Boolean = true, + external: Boolean = true): List { + return this.filter { it.external.size >= 3 }.map { + it.toShape(closed, internal, external) } -} \ No newline at end of file +} + +fun List.toShapeContours(closed: Boolean = false, + internal: Boolean = true, + external: Boolean = true): List { + val contours = mutableListOf() + this.forEach { contour -> + if(contour.external.size >= 3) { + if (external) { + val externalPoints = contour.external.toVector2s() + contours.add(ShapeContour.fromPoints(externalPoints, closed)) + } + if (internal) { + val internalCurves = contour.internal.filter { it.size >= 3 } + .map { it.toVector2s() } + internalCurves.forEach { internalContour -> + contours.add(ShapeContour.fromPoints(internalContour, closed)) + } + } + } + } + return contours +}