[orx-integral-image] Pad source image for npot inputs. Add demo

This commit is contained in:
Edwin Jakobs
2024-06-28 11:12:38 +02:00
parent ab04e6b001
commit b1f2984b3e
4 changed files with 133 additions and 84 deletions

View File

@@ -2,62 +2,3 @@
CPU and GPU-based implementation for integral images (summed area tables) CPU and GPU-based implementation for integral images (summed area tables)
#### Usage
```kotlin
val image = colorBuffer( ... )
image.shadow.download()
val integralImage = IntegralImage.fromColorBufferShadow(image.shadow)
// -- the sum for a given area can be queried using
val sum = integralImage.sum(IntRectangle(20, 20, 100, 100))
```
## Fast Integral Image
Since v0.0.20 orx-integral-image comes with `FastIntegralImage` which calculates integral images fully on the GPU.
```kotlin
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.integralimage.*
fun main(args: Array<String>) = application {
configure {
width = 1024
height = 1024
}
program {
val fii = FastIntegralImage()
val target = colorBuffer(1024, 512, 1.0, ColorFormat.RGBa, ColorType.FLOAT32)
val rt = renderTarget(1024, 512) {
colorBuffer()
}
extend {
drawer.background(ColorRGBa.PINK)
drawer.isolatedWithTarget(rt) {
drawer.ortho(rt)
drawer.background(ColorRGBa.BLACK)
drawer.fill = ColorRGBa.PINK.shade(1.0)
drawer.circle(mouse.position, 128.0)
}
fii.apply(rt.colorBuffer(0),target)
// -- here we sample from the integral image
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
float w = 128.0;
vec2 step = 1.0 / textureSize(image, 0);
vec4 t11 = texture(image, va_texCoord0 + step * vec2(w,w));
vec4 t01 = texture(image, va_texCoord0 + step * vec2(-w,w));
vec4 t00 = texture(image, va_texCoord0 + step * vec2(-w,-w));
vec4 t10 = texture(image, va_texCoord0 + step * vec2(w,-w));
x_fill = (t11 - t01 - t10 + t00) / (w*w);
""".trimIndent()
}
drawer.image(target)
}
}
}
```

View File

@@ -0,0 +1,42 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.integralimage.*
fun main() = application {
configure {
width = 720
height = 720
}
program {
val fii = FastIntegralImage()
val target = colorBuffer(width, height, 1.0, ColorFormat.RGBa, ColorType.FLOAT32)
val rt = renderTarget(width, height) {
colorBuffer()
}
extend {
drawer.clear(ColorRGBa.PINK)
drawer.isolatedWithTarget(rt) {
drawer.ortho(rt)
drawer.clear(ColorRGBa.BLACK)
drawer.fill = ColorRGBa.PINK.shade(1.0)
drawer.circle(mouse.position, 128.0)
}
fii.apply(rt.colorBuffer(0), target)
// -- here we sample from the integral image
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
float w = 64.0;
vec2 step = 1.0 / vec2(textureSize(image, 0));
vec4 t11 = texture(image, va_texCoord0 + step * vec2(w+1.0,w+1.0));
vec4 t01 = texture(image, va_texCoord0 + step * vec2(-w,w+1.0));
vec4 t00 = texture(image, va_texCoord0 + step * vec2(-w,-w));
vec4 t10 = texture(image, va_texCoord0 + step * vec2(w+1.0,-w));
x_fill = (t11 - t01 - t10 + t00) / ((2.0 * w +1.0) * (2.0 * w + 1.0));
""".trimIndent()
}
drawer.image(target)
}
}
}

View File

@@ -5,27 +5,43 @@ import org.openrndr.extra.fx.blend.Passthrough
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.resourceUrl import org.openrndr.resourceUrl
import org.openrndr.shape.IntRectangle
import org.openrndr.shape.Rectangle import org.openrndr.shape.Rectangle
import kotlin.math.ceil
import kotlin.math.log
class FastIntegralImageFilter : Filter(filterShaderFromUrl(resourceUrl( internal class FastIntegralImageFilter : Filter(
"/shaders/gl3/integral-image.frag" filterShaderFromUrl(
))) { resourceUrl(
"/shaders/gl3/integral-image.frag"
)
)
) {
var passIndex: Int by parameters var passIndex: Int by parameters
var passDirection: Vector2 by parameters var passDirection: Vector2 by parameters
var sampleCount: Int by parameters var sampleCount: Int by parameters
var sampleCountBase: Int by parameters var sampleCountBase: Int by parameters
} }
class FastIntegralImage : Filter(filterShaderFromUrl(resourceUrl( /**
"/shaders/gl3/integral-image.frag" * Compute an integral image for the source image
))) { */
class FastIntegralImage : Filter(
filterShaderFromUrl(
resourceUrl(
"/shaders/gl3/integral-image.frag"
)
)
) {
private val passthrough = Passthrough() private val passthrough = Passthrough()
var intermediate: ColorBuffer? = null var intermediate: ColorBuffer? = null
val filter = FastIntegralImageFilter() var sourceCropped: ColorBuffer? = null
var targetPadded: ColorBuffer? = null
private val filter = FastIntegralImageFilter()
private fun sampleCounts(size:Int, sampleCountBase:Int) : List<Int> { private fun sampleCounts(size: Int, sampleCountBase: Int): List<Int> {
var remainder = size var remainder = size
val sampleCounts = mutableListOf<Int>() val sampleCounts = mutableListOf<Int>()
while (remainder > 0) { while (remainder > 0) {
@@ -41,40 +57,90 @@ class FastIntegralImage : Filter(filterShaderFromUrl(resourceUrl(
override fun apply(source: Array<ColorBuffer>, target: Array<ColorBuffer>, clip: Rectangle?) { override fun apply(source: Array<ColorBuffer>, target: Array<ColorBuffer>, clip: Rectangle?) {
require(clip == null) require(clip == null)
val sampleCountBase = 16 require(source[0].isEquivalentTo(target[0], ignoreFormat = true, ignoreType = true))
val xSampleCounts = sampleCounts(source[0].width, sampleCountBase)
val ySampleCounts = sampleCounts(source[0].height, sampleCountBase)
val li = intermediate val npotx = ceil(log(source[0].effectiveWidth.toDouble(), 2.0)).toInt()
if (li == null || (li.width != source[0].width || li.height != source[0].height)) { val npoty = ceil(log(source[0].effectiveHeight.toDouble(), 2.0)).toInt()
intermediate?.destroy()
intermediate = colorBuffer(source[0].width, source[0].height, 1.0, ColorFormat.RGBa, ColorType.FLOAT32) val recWidth = 1 shl npotx
val recHeight = 1 shl npoty
if (recWidth != source[0].effectiveWidth || recHeight != source[0].effectiveHeight) {
if (sourceCropped?.effectiveWidth != recWidth || sourceCropped?.effectiveHeight != recHeight) {
sourceCropped?.destroy()
targetPadded?.destroy()
}
if (sourceCropped == null) {
sourceCropped = source[0].createEquivalent(width = recWidth, height = recHeight, contentScale = 1.0)
targetPadded = target[0].createEquivalent(
width = (recWidth / target[0].contentScale).toInt(),
height = (recHeight / target[0].contentScale).toInt(),
contentScale = 1.0
)
}
source[0].copyTo(sourceCropped!!,
sourceRectangle = IntRectangle(0, 0, source[0].effectiveWidth, source[0].effectiveHeight),
targetRectangle = IntRectangle(0, recHeight-source[0].effectiveHeight, source[0].effectiveWidth, source[0].effectiveHeight)
)
} }
val targets = arrayOf(target, arrayOf(intermediate!!)) val sampleCountBase = 16
val xSampleCounts = sampleCounts(recWidth, sampleCountBase)
val ySampleCounts = sampleCounts(recHeight, sampleCountBase)
val li = intermediate
if (li == null || (li.effectiveWidth != recWidth || li.effectiveHeight != recHeight)) {
intermediate?.destroy()
intermediate = colorBuffer(recWidth, recHeight, 1.0, ColorFormat.RGBa, ColorType.FLOAT32)
}
val targets = arrayOf(if (targetPadded == null) target else arrayOf(targetPadded!!), arrayOf(intermediate!!))
var targetIndex = 0 var targetIndex = 0
filter.sampleCountBase = sampleCountBase filter.sampleCountBase = sampleCountBase
/*
Perform horizontal steps
*/
filter.passDirection = Vector2.UNIT_X filter.passDirection = Vector2.UNIT_X
for (pass in xSampleCounts.indices) { for (pass in xSampleCounts.indices) {
filter.sampleCount = xSampleCounts[pass] filter.sampleCount = xSampleCounts[pass]
filter.passIndex = pass filter.passIndex = pass
filter.apply( if (pass == 0) source else targets[targetIndex%2], targets[(targetIndex+1)%2]) filter.apply(
if (pass == 0) {
if (sourceCropped == null) source else arrayOf(sourceCropped!!)
} else targets[targetIndex % 2], targets[(targetIndex + 1) % 2]
)
targetIndex++ targetIndex++
} }
/*
Perform vertical steps
*/
filter.passDirection = Vector2.UNIT_Y filter.passDirection = Vector2.UNIT_Y
for (pass in ySampleCounts.indices) { for (pass in ySampleCounts.indices) {
filter.sampleCount = ySampleCounts[pass] filter.sampleCount = ySampleCounts[pass]
filter.passIndex = pass filter.passIndex = pass
filter.apply( targets[targetIndex%2], targets[(targetIndex+1)%2]) filter.apply(targets[targetIndex % 2], targets[(targetIndex + 1) % 2])
targetIndex++ targetIndex++
} }
if (targetIndex%2 == 1) { // this is a bit wasteful
if (targetIndex % 2 == 1) {
passthrough.apply(targets[1], targets[0]) passthrough.apply(targets[1], targets[0])
} }
/*
When the source is not a power of two we copy from the padded target to the target
*/
if (targetPadded != null) {
targetPadded!!.copyTo(target[0],
sourceRectangle = IntRectangle(0, recHeight-source[0].effectiveHeight, source[0].effectiveWidth, source[0].effectiveHeight),
targetRectangle = IntRectangle(0, 0, source[0].effectiveWidth, source[0].effectiveHeight)
)
}
} }
} }

View File

@@ -1,5 +1,3 @@
#version 330 core
uniform sampler2D tex0; uniform sampler2D tex0;
in vec2 v_texCoord0; in vec2 v_texCoord0;
out vec4 o_color; out vec4 o_color;
@@ -11,16 +9,18 @@ uniform vec2 passDirection;
void main() { void main() {
vec2 passOffset = vec2(pow(sampleCountBase, passIndex)) * (1.0/textureSize(tex0, 0)) * passDirection; vec2 passOffset = vec2(
pow(float(sampleCountBase),
float(passIndex))) * (1.0 / vec2(textureSize(tex0, 0))
) * passDirection;
vec2 uv0 = v_texCoord0; vec2 uv0 = v_texCoord0;
// uv0.y = 1.0 - uv0.y;
vec4 result = vec4(0.0); vec4 result = vec4(0.0);
for (int i = 0; i < sampleCount; ++i) { for (int i = 0; i < sampleCount; ++i) {
vec2 readUV = v_texCoord0 - vec2(i *passOffset); vec2 readUV = v_texCoord0 - vec2(float(i) * passOffset);
float factor = step(0.0, readUV.x) * step(0.0, readUV.y); float factor = step(0.0, readUV.x) * step(0.0, readUV.y);
result += factor * texture(tex0, readUV); result += factor * texture(tex0, readUV);
} }
o_color = result; o_color = result;
} }