Skip to content

OpticalFlowSmoke.kt

src/jvmMain/kotlin/afield/media/vision/OpticalFlowSmoke.kt · 80 lines

package afield.media.vision

import afield.document.graph.Box
import afield.graphics.vulkan.Renderer
import afield.media.reconstruction.ReconstructionCapture
import afield.graphics.vulkan.util.Options
import afield.graphics.vulkan.util.Window
import org.json.JSONObject
import java.io.File
import kotlin.math.abs
import kotlin.system.exitProcess

object OpticalFlowSmoke

@JvmStatic fun main(args: Array<String>)

body — 66 lines, from line 14
    @JvmStatic fun main(args: Array<String>) {
        Options.enableVR=false;Options.enableHDR=false;Options.enableRayTracing=false;Options.doVulkanSDK()
        val dir=File("build/neural-tools/flow").also { it.mkdirs() }
        val window=Window("Field optical flow");val renderer=Renderer(window);val box=Box("flow-check")
        box.setPropertyRaw("renderer",renderer,doResync=false)
        try { Box.withBoxContextNow(box) {
            OpticalFlowDemo(renderer,backend=OpticalFlowBackend.HARDWARE).use { demo ->
                repeat(12){demo.update(reset=it==0);window.pollEvents();renderer.render()}
                val pixels=ReconstructionCapture.read(renderer,demo.flow.output)
                val errors=mutableListOf<Float>();val costs=mutableListOf<Float>()
                for(y in 16 until demo.flow.flowHeight-16)for(x in 16 until demo.flow.flowWidth-16) {
                    val i=(y*demo.flow.flowWidth+x)*4;errors+=abs(pixels[i]-demo.dx)+abs(pixels[i+1]-demo.dy);costs+=pixels[i+2]
                }
                errors.sort();check(errors[errors.size/2]<1f) { "Flow direction/units wrong: median L1 error ${errors[errors.size/2]}" }
                ReconstructionCapture.save(File(dir,"translation.png"),ReconstructionCapture.read(renderer,demo.output),demo.width*2,demo.height*2)
                val bytes=java.nio.ByteBuffer.allocate(pixels.size*4).order(java.nio.ByteOrder.LITTLE_ENDIAN);bytes.asFloatBuffer().put(pixels);File(dir,"flow.f32").writeBytes(bytes.array())
                val report=JSONObject().put("input",listOf(demo.width,demo.height)).put("grid",demo.flow.grid)
                    .put("expectedDisplacement",listOf(demo.dx,demo.dy)).put("medianL1ErrorPixels",errors[errors.size/2])
                    .put("p95L1ErrorPixels",errors[(errors.size*.95).toInt()]).put("costRange",listOf(costs.min(),costs.max()))
                    .put("allocatedBytes",demo.flow.allocatedBytes)
                repeat(100){demo.update();window.pollEvents();renderer.render()}
                val times=demo.flow.milliseconds.takeLast(100).sorted()
                report.put("pairWallMsIncludingConversionAndFence",JSONObject().put("count",times.size).put("p50",times[50]).put("p95",times[95]))
                report.put("opticalQueueTimestampBits",(demo.flow as HardwareOpticalFlow).opticalTimestampBits)
                demo.flow.gpuTimings.forEach { (name,values) -> val sorted=values.takeLast(100).sorted()
                    report.put(name+"GpuMs",if(sorted.isEmpty())JSONObject.NULL else JSONObject().put("count",sorted.size).put("p50",sorted[sorted.size/2]).put("p95",sorted[(sorted.size*.95).toInt()])) }
                demo.deformation=true;demo.dx=0f;demo.dy=0f;demo.displayRange=3f
                val localErrors=mutableListOf<Float>();val constantErrors=mutableListOf<Float>()
                val quadrants=IntArray(4)
                for(time in listOf(0f,.4f,.9f,1.5f)) {
                    demo.update(time,reset=true)
                    val field=ReconstructionCapture.read(renderer,demo.flow.output)
                    check(field.all { it.isFinite() })
                    for(y in 16 until demo.flow.flowHeight-16)for(x in 16 until demo.flow.flowWidth-16) {
                        val i=(y*demo.flow.flowWidth+x)*4
                        val expected=demo.expectedDisplacement((x+.5f)*demo.flow.grid,(y+.5f)*demo.flow.grid,time)
                        val ex=field[i]-expected.x;val ey=field[i+1]-expected.y
                        localErrors+=kotlin.math.sqrt(ex*ex+ey*ey)
                        constantErrors+=kotlin.math.sqrt((12-expected.x)*(12-expected.x)+(-8-expected.y)*(-8-expected.y))
                        if(kotlin.math.abs(field[i])>.2 && kotlin.math.abs(field[i+1])>.2)
                            quadrants[(if(field[i]>0)1 else 0)+(if(field[i+1]>0)2 else 0)]++
                    }
                }
                localErrors.sort();constantErrors.sort()
                val wiggle=JSONObject().put("pairStepSeconds",demo.pairStep).put("samples",localErrors.size)
                    .put("medianEndpointErrorPixels",localErrors[localErrors.size/2]).put("p95EndpointErrorPixels",localErrors[(localErrors.size*.95).toInt()])
                    .put("oldConstantVectorMedianErrorPixels",constantErrors[constantErrors.size/2]).put("directionQuadrants",quadrants.toList())
                report.put("consecutiveFrameWiggle",wiggle);println("WIGGLE METRICS $wiggle")
                check(localErrors[localErrors.size/2]<.5f && localErrors[(localErrors.size*.95).toInt()]<1.5f) { "Wiggle field disagrees with the known local warp: $wiggle" }
                check(quadrants.all { it>localErrors.size/20 }) { "Wiggle field did not change direction" }
                demo.pairStep=0f;demo.update(.5f,reset=true)
                val stationary=ReconstructionCapture.read(renderer,demo.flow.output)
                check(stationary.indices.filter { it%4<2 }.maxOf { abs(stationary[it]) }<.25f) { "Identical frames produced motion" }
                demo.pairStep=1f/30f
                repeat(if("--clip" in args)120 else 32){i ->demo.update(i*demo.pairStep,reset=i==0);window.pollEvents();renderer.render()
                    if("--clip" in args)ReconstructionCapture.save(File(dir,"clip/%04d.png".format(i)),ReconstructionCapture.read(renderer,demo.output),demo.width*2,demo.height*2)}
                ReconstructionCapture.save(File(dir,"advection.png"),ReconstructionCapture.read(renderer,demo.output),demo.width*2,demo.height*2)
                File(dir,"measurements.json").writeText(report.toString(2))
            }
            // A stopped parent renderer must also retire an otherwise still-reachable flow view.
            val unclosed=OpticalFlowDemo(renderer,128,128,name="unclosed-flow-check",backend=OpticalFlowBackend.HARDWARE)
            unclosed.update(reset=true)
        } } finally {renderer.cleanup();window.cleanup()}
        println("OPTICAL FLOW CHECK DONE");exitProcess(0)
    }
}