computer-vision-project/src/main/kotlin/network/rs485/ben/computervision/Golden.kt

121 lines
4.5 KiB
Kotlin

package network.rs485.ben.computervision
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import java.nio.file.Path
import kotlin.math.roundToInt
internal const val DISTANCE_DEVIATION: Double = 1.0
internal const val DISTANCE_STEP = 0.5
internal const val DILATION_SIZE = 1.0
fun interface DeviationFunction {
fun calculate(level: Int, value: Double): Double
}
data class DeviationParameters(val deviationMin: Int, val deviationFunc: DeviationFunction, val maximumDeviation: Double) {
val deviationRange = deviationMin until HIST_SIZE
}
class Golden(
val goldenName: String,
val goldenColor: Scalar,
entity: Entity,
val image: Mat,
val displayImage: Mat,
val windowManager: IWindowManager,
val deviationParameters: DeviationParameters,
) : IdentifiableEntity(entity, ImageDimensionProvider(image)) {
companion object {
suspend fun createGolden(
parentScope: CoroutineScope,
scanner: EntityScanner,
goldenName: String,
goldenPath: Path,
goldenColor: Scalar,
deviationParameters: DeviationParameters,
): Golden {
val windowMgr = WINDOW_MANAGER_FACTORY.createWindowManager(goldenName)
val (displayImage, image) = CvContext.readAndConvertImage(goldenPath)
parentScope.launch {
windowMgr.display(displayImage)
}
windowMgr.setDimension(256, 256)
val entity =
scanner.scanImage(image, displayImage, windowMgr)
.takeIf { it.size == 1 }
?.get(0)
?: throw RuntimeException("Cannot find $goldenName in golden image")
val golden = Golden(
goldenName = goldenName,
goldenColor = goldenColor,
entity = entity,
image = image,
displayImage = displayImage,
windowManager = windowMgr,
deviationParameters = deviationParameters,
)
golden.distanceMat.copyToColor(displayImage)
parentScope.launch {
golden.updateImage()
}
return golden
}
}
val orientation: Line = run {
val midX = minMaxDistance.maxLoc.x.roundToInt()
val midY = minMaxDistance.maxLoc.y.roundToInt()
val midRounded = Pixel(midX, midY)
if (minMaxDistance.maxLoc != midRounded.vec2D()) println("${minMaxDistance.maxLoc} != ${midRounded.vec2D()}")
transformedLines.filter { it.first.y >= midY }
.flatMap { (first, second) -> listOf(first, second) }
.map { startPixel ->
val endPixel = entityMask.searchGreatest(
center = midRounded,
direction = (midRounded - startPixel).vec2D().normalize(),
searchRange = (0..(cols + rows)) // use distance step?
) ?: throw RuntimeException(
"Could not find any border for $goldenName center $midRounded in $entityMask:\n" +
entityMask.dump()
)
startPixel to endPixel
}
.maxByOrNull { it.vec2D().magnitude }
?: throw IllegalStateException("Could not find orientation for $goldenName")
}
val orientationRadius = orientation.vec2D().magnitude / 2.0
val entityMaskDilated: Mat = Mat(rows, cols, entityMask.type()).also { dest ->
val dilationKernel = Imgproc.getStructuringElement(
/* shape = */ Imgproc.MORPH_ELLIPSE,
/* ksize = */ Size(2 * DILATION_SIZE + 1, 2 * DILATION_SIZE + 1),
)
Imgproc.dilate(entityMask, dest, dilationKernel)
}
internal inline fun <T> withCentrumMask(identity: IdentifiableEntity, block: (Mat) -> T): T =
CvContext.withMat(MatInfo(identity.rows, identity.cols, CvType.CV_8UC1)) { maskMat ->
Core.inRange(
/* src = */ identity.distanceMat,
/* lowerb = */ Scalar(minMaxDistance.maxVal.minus(DISTANCE_DEVIATION)),
/* upperb = */ Scalar(minMaxDistance.maxVal.plus(DISTANCE_DEVIATION)),
/* dst = */ maskMat,
)
block(maskMat)
}
suspend fun updateImage() = windowManager.updateImage(displayImage)
override fun release() {
super.release()
entityMaskDilated.release()
windowManager.dispose()
}
override fun toString(): String = "Golden($goldenName)"
}