From 0747cfd01c9f77d21cf0c54fba51a4597f628ad3 Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Tue, 18 Aug 2020 21:56:29 +0200 Subject: [PATCH] [orx-color] Add color histograms --- orx-color/README.md | 16 ++++ orx-color/src/demo/kotlin/DemoHistogram01.kt | 30 +++++++ orx-color/src/demo/kotlin/DemoHistogram02.kt | 32 +++++++ orx-color/src/demo/kotlin/DemoHistogram03.kt | 29 +++++++ .../src/main/kotlin/statistics/Histogram.kt | 87 +++++++++++++++++++ 5 files changed, 194 insertions(+) create mode 100644 orx-color/README.md create mode 100644 orx-color/src/demo/kotlin/DemoHistogram01.kt create mode 100644 orx-color/src/demo/kotlin/DemoHistogram02.kt create mode 100644 orx-color/src/demo/kotlin/DemoHistogram03.kt create mode 100644 orx-color/src/main/kotlin/statistics/Histogram.kt diff --git a/orx-color/README.md b/orx-color/README.md new file mode 100644 index 00000000..7dce1118 --- /dev/null +++ b/orx-color/README.md @@ -0,0 +1,16 @@ +# orx-color + +Tools to work with color + +## Color presets + +orx-color adds an extensive list of preset colors to `ColorRGBa`. Check [sources](src/main/kotlin/presets/Colors.kt) for a listing of the preset colors. + +## Color histograms + +orx-color comes with tools to calculate color histograms for images. + +``` +val histogram = calculateHistogramRGB(image) +val colors = histogram.sortedColors() +``` \ No newline at end of file diff --git a/orx-color/src/demo/kotlin/DemoHistogram01.kt b/orx-color/src/demo/kotlin/DemoHistogram01.kt new file mode 100644 index 00000000..022db2bd --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoHistogram01.kt @@ -0,0 +1,30 @@ +// Show color histogram of an image + +import org.openrndr.application +import org.openrndr.draw.loadImage +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extras.color.statistics.calculateHistogramRGB + +fun main() = application { + program { + // -- this block is for automation purposes only + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + val image = loadImage("demo-data/images/image-001.png") + val histogram = calculateHistogramRGB(image) + val colors = histogram.sortedColors() + extend { + + drawer.image(image) + for (i in 0 until 32) { + drawer.fill = colors[i].first + drawer.stroke = null + drawer.rectangle(i * (width/32.0), height-16.0, width/32.0, 16.0) + } + + } + } +} \ No newline at end of file diff --git a/orx-color/src/demo/kotlin/DemoHistogram02.kt b/orx-color/src/demo/kotlin/DemoHistogram02.kt new file mode 100644 index 00000000..99826f8a --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoHistogram02.kt @@ -0,0 +1,32 @@ +// Show color histogram using non-uniform weighting + +import org.openrndr.application +import org.openrndr.draw.loadImage +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extras.color.statistics.calculateHistogramRGB +import kotlin.math.pow + +fun main() = application { + program { + // -- this block is for automation purposes only + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + val image = loadImage("demo-data/images/image-001.png") + // -- here we use non-uniform weighting, such that bright colors are prioritized + val histogram = calculateHistogramRGB(image, weighting = { + ((r+g+b)/3.0).pow(2.4) + }) + val colors = histogram.sortedColors() + extend { + drawer.image(image) + for (i in 0 until 32) { + drawer.fill = colors[i].first + drawer.stroke = null + drawer.rectangle(i * (width / 32.0), height - 16.0, width / 32.0, 16.0) + } + } + } +} \ No newline at end of file diff --git a/orx-color/src/demo/kotlin/DemoHistogram03.kt b/orx-color/src/demo/kotlin/DemoHistogram03.kt new file mode 100644 index 00000000..ad039251 --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoHistogram03.kt @@ -0,0 +1,29 @@ +// Create a simple rectangle composition based on colors sampled from image + +import org.openrndr.application +import org.openrndr.draw.loadImage +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extras.color.statistics.calculateHistogramRGB + +fun main() = application { + program { + // -- this block is for automation purposes only + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + val image = loadImage("demo-data/images/image-001.png") + val histogram = calculateHistogramRGB(image) + extend { + drawer.image(image) + for (j in 0 until height step 32) { + for (i in 0 until width step 32) { + drawer.stroke = null + drawer.fill = histogram.sample() + drawer.rectangle(i * 1.0, j * 1.0, 32.0, 32.0) + } + } + } + } +} \ No newline at end of file diff --git a/orx-color/src/main/kotlin/statistics/Histogram.kt b/orx-color/src/main/kotlin/statistics/Histogram.kt new file mode 100644 index 00000000..350ad3b0 --- /dev/null +++ b/orx-color/src/main/kotlin/statistics/Histogram.kt @@ -0,0 +1,87 @@ +package org.openrndr.extras.color.statistics + +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.ColorBuffer +import kotlin.random.Random + +private fun ColorRGBa.binIndex(binCount: Int): Triple { + val rb = (r * binCount).toInt().coerceIn(0, binCount - 1) + val gb = (g * binCount).toInt().coerceIn(0, binCount - 1) + val bb = (b * binCount).toInt().coerceIn(0, binCount - 1) + return Triple(rb, gb, bb) +} + +fun calculateHistogramRGB(buffer: ColorBuffer, + binCount: Int = 16, + weighting: ColorRGBa.() -> Double = { 1.0 }, + downloadShadow: Boolean = true): RGBHistogram { + val bins = Array(binCount) { Array(binCount) { DoubleArray(binCount) } } + if (downloadShadow) { + buffer.shadow.download() + } + + var totalWeight = 0.0 + val s = buffer.shadow + for (y in 0 until buffer.height) { + for (x in 0 until buffer.width) { + val c = s[x, y] + val weight = c.weighting() + val (rb, gb, bb) = c.binIndex(binCount) + bins[rb][gb][bb] += weight + totalWeight += weight + } + } + + if (totalWeight > 0) + for (r in 0 until binCount) { + for (g in 0 until binCount) { + for (b in 0 until binCount) { + bins[r][g][b] /= totalWeight + } + } + } + return RGBHistogram(bins, binCount) +} + + +class RGBHistogram(val freqs: Array>, val binCount: Int) { + fun frequency(color: ColorRGBa): Double { + val (rb, gb, bb) = color.binIndex(binCount) + return freqs[rb][gb][bb] + } + + fun color(rBin: Int, gBin: Int, bBin: Int): ColorRGBa = + ColorRGBa(rBin / (binCount - 1.0), gBin / (binCount - 1.0), bBin / (binCount - 1.0)) + + fun sample(random: Random = Random.Default): ColorRGBa { + val x = random.nextDouble() + var sum = 0.0 + for (r in 0 until binCount) { + for (g in 0 until binCount) { + for (b in 0 until binCount) { + sum += freqs[r][g][b] + if (sum >= x) { + return color(r, g, b) + } + } + } + } + return color(binCount - 1, binCount - 1, binCount - 1) + } + + fun sortedColors(): List> { + val result = mutableListOf>() + for (r in 0 until binCount) { + for (g in 0 until binCount) { + for (b in 0 until binCount) { + result += Pair( + ColorRGBa(r / (binCount - 1.0), g / (binCount - 1.0), b / (binCount - 1.0)), + freqs[r][g][b] + ) + } + } + } + return result.sortedByDescending { it.second } + } +} +