Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,46 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = 'analysis'
|
||||
spec.version = '1.0'
|
||||
spec.homepage = ''
|
||||
spec.source = { :http=> ''}
|
||||
spec.authors = ''
|
||||
spec.license = ''
|
||||
spec.summary = ''
|
||||
spec.vendored_frameworks = 'build/cocoapods/framework/inference.framework'
|
||||
spec.libraries = 'c++'
|
||||
spec.ios.deployment_target = '15.1'
|
||||
spec.dependency 'onnxruntime-objc', '1.26.0'
|
||||
if !Dir.exist?('build/cocoapods/framework/inference.framework') || Dir.empty?('build/cocoapods/framework/inference.framework')
|
||||
raise "
|
||||
Kotlin framework 'inference' doesn't exist yet, so a proper Xcode project can't be generated.
|
||||
'pod install' should be executed after running ':generateDummyFramework' Gradle task:
|
||||
./gradlew :tool:analysis:generateDummyFramework
|
||||
Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
|
||||
end
|
||||
spec.xcconfig = {
|
||||
'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
|
||||
}
|
||||
spec.pod_target_xcconfig = {
|
||||
'KOTLIN_PROJECT_PATH' => ':tool:analysis',
|
||||
'PRODUCT_MODULE_NAME' => 'inference',
|
||||
}
|
||||
spec.script_phases = [
|
||||
{
|
||||
:name => 'Build analysis',
|
||||
:execution_position => :before_compile,
|
||||
:shell_path => '/bin/sh',
|
||||
:script => <<-SCRIPT
|
||||
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
|
||||
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
|
||||
exit 0
|
||||
fi
|
||||
set -ev
|
||||
REPO_ROOT="$PODS_TARGET_SRCROOT"
|
||||
"$REPO_ROOT/../../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
|
||||
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
|
||||
-Pkotlin.native.cocoapods.archs="$ARCHS" \
|
||||
-Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
|
||||
SCRIPT
|
||||
}
|
||||
]
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.androidLint)
|
||||
kotlin("native.cocoapods")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "ru.tanexc.hemoananlysis.tool.analysis"
|
||||
|
||||
compileSdk {
|
||||
version = release(36) {
|
||||
minorApiLevel = 1
|
||||
}
|
||||
}
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
val xcfName = "inferenceKit"
|
||||
listOf(
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = xcfName
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
cocoapods {
|
||||
version = "1.0"
|
||||
ios.deploymentTarget = "15.1"
|
||||
|
||||
framework {
|
||||
baseName = "inference"
|
||||
isStatic = false
|
||||
}
|
||||
|
||||
pod("onnxruntime-objc", version = libs.versions.onnxruntime.get())
|
||||
}
|
||||
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api(projects.domain)
|
||||
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.koin.core)
|
||||
}
|
||||
}
|
||||
|
||||
androidMain {
|
||||
dependencies {
|
||||
implementation(libs.onnxruntime.android)
|
||||
implementation(libs.compose.components.resources)
|
||||
}
|
||||
}
|
||||
|
||||
iosMain {
|
||||
dependencies {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = 'inference'
|
||||
spec.version = '1.0'
|
||||
spec.homepage = ''
|
||||
spec.source = { :http=> ''}
|
||||
spec.authors = ''
|
||||
spec.license = ''
|
||||
spec.summary = ''
|
||||
spec.vendored_frameworks = 'build/cocoapods/framework/inference.framework'
|
||||
spec.libraries = 'c++'
|
||||
spec.ios.deployment_target = '15.1'
|
||||
spec.dependency 'onnxruntime-objc', '1.25.1'
|
||||
if !Dir.exist?('build/cocoapods/framework/inference.framework') || Dir.empty?('build/cocoapods/framework/inference.framework')
|
||||
raise "
|
||||
Kotlin framework 'inference' doesn't exist yet, so a proper Xcode project can't be generated.
|
||||
'pod install' should be executed after running ':generateDummyFramework' Gradle task:
|
||||
./gradlew :tool:inference:generateDummyFramework
|
||||
Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
|
||||
end
|
||||
spec.xcconfig = {
|
||||
'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
|
||||
}
|
||||
spec.pod_target_xcconfig = {
|
||||
'KOTLIN_PROJECT_PATH' => ':tool:inference',
|
||||
'PRODUCT_MODULE_NAME' => 'inference',
|
||||
}
|
||||
spec.script_phases = [
|
||||
{
|
||||
:name => 'Build inference',
|
||||
:execution_position => :before_compile,
|
||||
:shell_path => '/bin/sh',
|
||||
:script => <<-SCRIPT
|
||||
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
|
||||
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
|
||||
exit 0
|
||||
fi
|
||||
set -ev
|
||||
REPO_ROOT="$PODS_TARGET_SRCROOT"
|
||||
"$REPO_ROOT/../../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
|
||||
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
|
||||
-Pkotlin.native.cocoapods.archs="$ARCHS" \
|
||||
-Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
|
||||
SCRIPT
|
||||
}
|
||||
]
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl
|
||||
|
||||
import ai.onnxruntime.OnnxTensor
|
||||
import ai.onnxruntime.OrtEnvironment
|
||||
import ai.onnxruntime.OrtSession
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.MODEL_INPUT_HEIGHT
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.MODEL_INPUT_WIDTH
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.FloatBuffer
|
||||
|
||||
internal object OrtRuntime {
|
||||
private val environment: OrtEnvironment = OrtEnvironment.getEnvironment()
|
||||
|
||||
private var modelBuffer: ByteBuffer? = null
|
||||
private var options: OrtSession.SessionOptions? = null
|
||||
|
||||
@Synchronized
|
||||
fun createSession(modelStream: InputStream): OrtSession {
|
||||
val bytes = modelStream.use { it.readBytes() }
|
||||
modelBuffer = ByteBuffer.allocateDirect(bytes.size)
|
||||
.order(ByteOrder.nativeOrder())
|
||||
.apply {
|
||||
put(bytes)
|
||||
rewind()
|
||||
}
|
||||
|
||||
options = OrtSession.SessionOptions().apply {
|
||||
setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT)
|
||||
setMemoryPatternOptimization(true)
|
||||
|
||||
addCPU(true)
|
||||
|
||||
addConfigEntry("session.load_model_format", "ORT")
|
||||
addConfigEntry("session.use_ort_model_bytes_directly", "1")
|
||||
addConfigEntry("session.use_ort_model_bytes_for_initializers", "1")
|
||||
}
|
||||
|
||||
return environment.createSession(modelBuffer!!, options)
|
||||
}
|
||||
|
||||
fun createTensor(data: FloatBuffer): OnnxTensor = OnnxTensor.createTensor(
|
||||
environment,
|
||||
data,
|
||||
longArrayOf(1, 3, MODEL_INPUT_WIDTH.toLong(), MODEL_INPUT_HEIGHT.toLong())
|
||||
)
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.inference
|
||||
|
||||
import ai.onnxruntime.OnnxTensor
|
||||
import ai.onnxruntime.OrtSession
|
||||
import ai.onnxruntime.TensorInfo
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetLatestModelFileUseCase
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.OrtRuntime
|
||||
import java.nio.FloatBuffer
|
||||
import kotlin.getValue
|
||||
|
||||
internal actual class InferenceHandler : KoinComponent {
|
||||
private val getLatestModelFileUseCase: GetLatestModelFileUseCase by inject()
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
private lateinit var session: OrtSession
|
||||
|
||||
init {
|
||||
initSession()
|
||||
}
|
||||
|
||||
private fun initSession() {
|
||||
val isLatestModel = modelRepository.getCurrentModelInfo()?.isLatest?: false
|
||||
if (!isLatestModel || !::session.isInitialized) {
|
||||
getLatestModelFileUseCase()?.let { model ->
|
||||
session = OrtRuntime.createSession(modelStream = model.inputStream())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual operator fun invoke(tensorImage: FloatArray): InferenceOutput {
|
||||
initSession()
|
||||
if (!::session.isInitialized) error("Model does not exists")
|
||||
|
||||
val inputBuffer: FloatBuffer = FloatBuffer.wrap(tensorImage)
|
||||
val tensor = OrtRuntime.createTensor(inputBuffer)
|
||||
|
||||
val output = session.run(mapOf(session.inputNames.first() to tensor))
|
||||
return output.asInferenceOutput()
|
||||
}
|
||||
|
||||
private fun OrtSession.Result.asInferenceOutput(): InferenceOutput {
|
||||
val outputInfo = this[0].info as TensorInfo
|
||||
val outputShape = outputInfo.shape
|
||||
|
||||
val channelCount = outputShape[1].toInt()
|
||||
val candidateCount = outputShape[2].toInt()
|
||||
|
||||
val outputTensor = this[0] as OnnxTensor
|
||||
val outputData = outputTensor.value as Array<Array<FloatArray>>
|
||||
return InferenceOutput(
|
||||
channelCount = channelCount,
|
||||
candidateCount = candidateCount,
|
||||
outputData = outputData
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.AnalysisParameters
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.AnalysisResult
|
||||
|
||||
interface AnalysisTool {
|
||||
fun performAnalysis(parameters: AnalysisParameters): AnalysisResult
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api.domain
|
||||
|
||||
import kotlin.time.Duration
|
||||
|
||||
class AnalysisException(
|
||||
exception: Exception,
|
||||
val duration: Duration
|
||||
): RuntimeException(exception)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api.domain
|
||||
|
||||
data class ImageBuffer(
|
||||
val pixels: IntArray,
|
||||
val width: Int,
|
||||
val height: Int
|
||||
) {
|
||||
operator fun get(x: Int, y: Int): Int =
|
||||
pixels[y * width + x]
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
|
||||
data class AnalysisParameters(
|
||||
val imageBuffer: ImageBuffer,
|
||||
val confThreshold: Float,
|
||||
val iouThreshold: Float,
|
||||
val preprocessVariant: PreprocessVariant,
|
||||
val roiVariant: RoiVariant
|
||||
)
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters
|
||||
|
||||
enum class PreprocessVariant {
|
||||
Default, BrightnessContrast
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters
|
||||
|
||||
enum class RoiVariant {
|
||||
None, ThresholdMorph, ThresholdFilter
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api.domain.results
|
||||
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class AnalysisDuration(
|
||||
val preprocessing: Duration = Duration.ZERO,
|
||||
val inference: Duration = Duration.ZERO,
|
||||
val postprocessing: Duration = Duration.ZERO,
|
||||
) {
|
||||
val total: Duration = preprocessing + inference + postprocessing
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api.domain.results
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.AnalysisException
|
||||
|
||||
|
||||
sealed interface AnalysisResult {
|
||||
data class Success(
|
||||
val detection: List<Detection>,
|
||||
val duration: AnalysisDuration
|
||||
) : AnalysisResult
|
||||
|
||||
data class Error(
|
||||
val exception: AnalysisException
|
||||
) : AnalysisResult
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api.domain.results
|
||||
|
||||
enum class CellClass {
|
||||
Lymphocyte,
|
||||
Metamyelocyte,
|
||||
Monocyte,
|
||||
Myelocyte,
|
||||
Normoblasts,
|
||||
RodNeutrophil,
|
||||
SegmentonuclearNeutrophil,
|
||||
Blast,
|
||||
Promyelocyte,
|
||||
Eosinophil,
|
||||
Basophil,
|
||||
Magakariocyte,
|
||||
BadCells,
|
||||
PlasmaCell,
|
||||
Artifacts,
|
||||
GumprichtShadows,
|
||||
Methoses,
|
||||
NormoblastsOxyfilny,
|
||||
Macrophages,
|
||||
NormoblastsErythroblasts,
|
||||
NormoblastsBasophilic,
|
||||
NormoblastsPolychromatophilic
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.api.domain.results
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.CellClass
|
||||
|
||||
data class Detection(
|
||||
val cellClass: CellClass,
|
||||
val confidence: Float,
|
||||
val centerX: Float,
|
||||
val centerY: Float,
|
||||
val width: Float,
|
||||
val height: Float,
|
||||
)
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.AnalysisParameters
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.AnalysisException
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.inference.InferenceHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.postprocess.PostprocessHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.preprocess.PreprocessHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.roi.RoiHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.tensor.TensorHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.AnalysisDuration
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.AnalysisResult
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.AnalysisTool
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.measuresDuration
|
||||
|
||||
internal class AnalysisToolImpl(
|
||||
private val preprocessHandler: PreprocessHandler,
|
||||
private val roiHandler: RoiHandler,
|
||||
private val tensorHandler: TensorHandler,
|
||||
private val inferenceHandler: InferenceHandler,
|
||||
private val postprocessHandler: PostprocessHandler,
|
||||
): AnalysisTool {
|
||||
override fun performAnalysis(parameters: AnalysisParameters): AnalysisResult = try {
|
||||
val sourceImageBuffer = parameters.imageBuffer
|
||||
|
||||
val (preprocessOutput, preprocessDuration) = measuresDuration {
|
||||
preprocessHandler(
|
||||
imageBuffer = sourceImageBuffer,
|
||||
preprocessVariant = parameters.preprocessVariant
|
||||
)
|
||||
}
|
||||
|
||||
val (roiOutput, roiDuration) = measuresDuration {
|
||||
roiHandler(
|
||||
imageBuffer = preprocessOutput.imageBuffer,
|
||||
roiVariant =parameters.roiVariant
|
||||
)
|
||||
}
|
||||
|
||||
val (tensorOutput, tensorDuration) = measuresDuration {
|
||||
tensorHandler(imageBuffer = roiOutput.imageBuffer)
|
||||
}
|
||||
|
||||
val (inferenceOutput, inferenceDuration) = measuresDuration {
|
||||
inferenceHandler(tensorImage = tensorOutput)
|
||||
}
|
||||
|
||||
val (postprocessOutput, postprocessDuration) = measuresDuration {
|
||||
postprocessHandler(
|
||||
inferenceOutput = inferenceOutput,
|
||||
letterboxMeta = preprocessOutput.meta,
|
||||
confThreshold = parameters.confThreshold,
|
||||
iouThreshold = parameters.iouThreshold
|
||||
)
|
||||
}
|
||||
|
||||
val duration = AnalysisDuration(
|
||||
preprocessing = preprocessDuration + roiDuration + tensorDuration,
|
||||
inference = inferenceDuration,
|
||||
postprocessing = postprocessDuration
|
||||
)
|
||||
|
||||
return AnalysisResult.Success(
|
||||
detection = postprocessOutput.detections,
|
||||
duration = duration
|
||||
)
|
||||
} catch (e: AnalysisException) {
|
||||
return AnalysisResult.Error(e)
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.core
|
||||
|
||||
internal const val MODEL_INPUT_WIDTH = 640
|
||||
internal const val MODEL_INPUT_HEIGHT = 640
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.core
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
|
||||
internal val BACKGROUND_COLOR = rgb(200, 200, 200)
|
||||
|
||||
internal fun rgb(
|
||||
r: Int,
|
||||
g: Int,
|
||||
b: Int,
|
||||
a: Int = 255
|
||||
): Int =
|
||||
((a.coerceIn(0, 255) and 0xFF) shl 24) or
|
||||
((r.coerceIn(0, 255) and 0xFF) shl 16) or
|
||||
((g.coerceIn(0, 255) and 0xFF) shl 8) or
|
||||
(b.coerceIn(0, 255) and 0xFF)
|
||||
|
||||
internal fun red(pixel: Int): Int =
|
||||
pixel shr 16 and 0xFF
|
||||
|
||||
internal fun green(pixel: Int): Int =
|
||||
pixel shr 8 and 0xFF
|
||||
|
||||
internal fun blue(pixel: Int): Int =
|
||||
pixel and 0xFF
|
||||
|
||||
|
||||
fun gray(pixel: Int): Int {
|
||||
val r = pixel ushr 16 and 0xFF
|
||||
val g = pixel ushr 8 and 0xFF
|
||||
val b = pixel and 0xFF
|
||||
return ((r * 299 + g * 587 + b * 114) / 1000).coerceIn(0, 255)
|
||||
}
|
||||
|
||||
|
||||
internal fun open(mask: BooleanArray, width: Int, height: Int): BooleanArray =
|
||||
dilate(erode(mask, width, height), width, height)
|
||||
|
||||
internal fun close(mask: BooleanArray, width: Int, height: Int): BooleanArray =
|
||||
erode(dilate(mask, width, height), width, height)
|
||||
|
||||
internal fun erode(mask: BooleanArray, width: Int, height: Int): BooleanArray {
|
||||
val result = BooleanArray(mask.size)
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
var keep = true
|
||||
for (dy in -2..2) {
|
||||
for (dx in -2..2) {
|
||||
val nx = x + dx
|
||||
val ny = y + dy
|
||||
if (nx !in 0 until width || ny !in 0 until height || !mask[ny * width + nx]) {
|
||||
keep = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!keep) break
|
||||
}
|
||||
result[y * width + x] = keep
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
internal fun dilate(mask: BooleanArray, width: Int, height: Int): BooleanArray {
|
||||
val result = BooleanArray(mask.size)
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
var keep = false
|
||||
for (dy in -2..2) {
|
||||
for (dx in -2..2) {
|
||||
val nx = x + dx
|
||||
val ny = y + dy
|
||||
if (nx in 0 until width && ny in 0 until height && mask[ny * width + nx]) {
|
||||
keep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (keep) break
|
||||
}
|
||||
result[y * width + x] = keep
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
internal fun otsuThreshold(imageBuffer: ImageBuffer): Int {
|
||||
val histogram = IntArray(256)
|
||||
imageBuffer.pixels.forEach { histogram[gray(it)]++ }
|
||||
val total = imageBuffer.pixels.size
|
||||
var sum = 0L
|
||||
for (i in histogram.indices) sum += i.toLong() * histogram[i]
|
||||
|
||||
var backgroundWeight = 0
|
||||
var backgroundSum = 0L
|
||||
var bestThreshold = 0
|
||||
var bestVariance = -1.0
|
||||
|
||||
for (threshold in histogram.indices) {
|
||||
backgroundWeight += histogram[threshold]
|
||||
if (backgroundWeight == 0) continue
|
||||
|
||||
val foregroundWeight = total - backgroundWeight
|
||||
if (foregroundWeight == 0) break
|
||||
|
||||
backgroundSum += threshold.toLong() * histogram[threshold]
|
||||
val backgroundMean = backgroundSum.toDouble() / backgroundWeight
|
||||
val foregroundMean = (sum - backgroundSum).toDouble() / foregroundWeight
|
||||
val variance = backgroundWeight.toDouble() *
|
||||
foregroundWeight *
|
||||
(backgroundMean - foregroundMean) *
|
||||
(backgroundMean - foregroundMean)
|
||||
|
||||
if (variance > bestVariance) {
|
||||
bestVariance = variance
|
||||
bestThreshold = threshold
|
||||
}
|
||||
}
|
||||
|
||||
return bestThreshold
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.core
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.AnalysisException
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.TimeSource.Monotonic.markNow
|
||||
|
||||
@Throws(AnalysisException::class)
|
||||
inline fun <T> measuresDuration(block: () -> T): Pair<T, Duration> {
|
||||
val mark = markNow()
|
||||
try {
|
||||
val result = block()
|
||||
return result to mark.elapsedNow()
|
||||
} catch (exception: Exception) {
|
||||
val elapsed = mark.elapsedNow()
|
||||
throw AnalysisException(
|
||||
exception = exception,
|
||||
duration = elapsed,
|
||||
)
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.data
|
||||
|
||||
internal data class LetterboxMeta(
|
||||
val gain: Float,
|
||||
val padX: Float,
|
||||
val padY: Float,
|
||||
val left: Int,
|
||||
val top: Int,
|
||||
)
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.di
|
||||
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.AnalysisToolImpl
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.inference.InferenceHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.postprocess.PostprocessHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.preprocess.PreprocessHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.roi.RoiHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.tensor.TensorHandler
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.AnalysisTool
|
||||
|
||||
val analysisToolModule = module {
|
||||
singleOf(::PreprocessHandler)
|
||||
singleOf(::RoiHandler)
|
||||
singleOf(::TensorHandler)
|
||||
singleOf(::InferenceHandler)
|
||||
singleOf(::PostprocessHandler)
|
||||
singleOf(::AnalysisToolImpl) bind AnalysisTool::class
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.inference
|
||||
|
||||
|
||||
internal expect class InferenceHandler() {
|
||||
operator fun invoke(tensorImage: FloatArray): InferenceOutput
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.inference
|
||||
|
||||
|
||||
internal data class InferenceOutput(
|
||||
val channelCount: Int,
|
||||
val candidateCount: Int,
|
||||
val outputData: Array<Array<FloatArray>>
|
||||
)
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.postprocess
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.inference.InferenceOutput
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.data.LetterboxMeta
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.CellClass
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.Detection
|
||||
import kotlin.collections.plusAssign
|
||||
import kotlin.math.abs
|
||||
|
||||
internal class PostprocessHandler {
|
||||
operator fun invoke(
|
||||
inferenceOutput: InferenceOutput,
|
||||
letterboxMeta: LetterboxMeta,
|
||||
confThreshold: Float,
|
||||
iouThreshold: Float
|
||||
): PostprocessOutput {
|
||||
val detections = process(
|
||||
inferenceOutput = inferenceOutput,
|
||||
letterboxMeta = letterboxMeta,
|
||||
confThreshold = confThreshold,
|
||||
iouThreshold = iouThreshold
|
||||
)
|
||||
|
||||
return PostprocessOutput(
|
||||
detections = detections
|
||||
)
|
||||
}
|
||||
|
||||
private fun process(
|
||||
inferenceOutput: InferenceOutput,
|
||||
letterboxMeta: LetterboxMeta,
|
||||
confThreshold: Float,
|
||||
iouThreshold: Float
|
||||
): List<Detection> {
|
||||
val channelCount = inferenceOutput.channelCount
|
||||
val candidateCount = inferenceOutput.candidateCount
|
||||
|
||||
val rawPredictions = inferenceOutput.outputData[0]
|
||||
|
||||
val classScoresStartIndex = 4
|
||||
val classCount = channelCount - classScoresStartIndex
|
||||
|
||||
val candidates = mutableListOf<Detection>()
|
||||
|
||||
for (candidateIdx in 0 until candidateCount) {
|
||||
val x = rawPredictions[0][candidateIdx]
|
||||
val y = rawPredictions[1][candidateIdx]
|
||||
val width = rawPredictions[2][candidateIdx]
|
||||
val height = rawPredictions[3][candidateIdx]
|
||||
|
||||
var bestClassId = -1
|
||||
var bestScore = Float.NEGATIVE_INFINITY
|
||||
|
||||
for (classIdx in 0 until classCount) {
|
||||
val score = rawPredictions[classScoresStartIndex + classIdx][candidateIdx]
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score
|
||||
bestClassId = classIdx
|
||||
}
|
||||
}
|
||||
|
||||
if (bestScore < confThreshold) continue
|
||||
if (bestClassId !in CellClass.entries.indices) continue
|
||||
|
||||
candidates += Detection(
|
||||
cellClass = CellClass.entries[bestClassId],
|
||||
confidence = bestScore,
|
||||
centerX = x,
|
||||
centerY = y,
|
||||
width = width,
|
||||
height = height
|
||||
)
|
||||
}
|
||||
|
||||
val originalSpaceDetections = candidates.map { detection ->
|
||||
detection.toOriginalSpace(meta = letterboxMeta)
|
||||
}
|
||||
|
||||
return applyNms(
|
||||
detections = originalSpaceDetections,
|
||||
iouThreshold = iouThreshold
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyNms(
|
||||
detections: List<Detection>,
|
||||
iouThreshold: Float
|
||||
): List<Detection> {
|
||||
return detections
|
||||
.groupBy { it.cellClass }
|
||||
.flatMap { (_, value) ->
|
||||
val remainingItems = value
|
||||
.sortedBy { it.confidence }
|
||||
.toMutableList()
|
||||
|
||||
var index1 = 0
|
||||
|
||||
while (index1 < remainingItems.size) {
|
||||
val currentItem = remainingItems[index1]
|
||||
|
||||
var index2 = index1 + 1
|
||||
var isDeleted = false
|
||||
|
||||
while (index2 < remainingItems.size) {
|
||||
val nextItem = remainingItems[index2]
|
||||
|
||||
if (iou(nextItem, currentItem) > iouThreshold) {
|
||||
remainingItems.removeAt(index1)
|
||||
isDeleted = true
|
||||
break
|
||||
} else {
|
||||
index2++
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDeleted) {
|
||||
index1++
|
||||
}
|
||||
}
|
||||
|
||||
remainingItems.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Detection.toOriginalSpace(meta: LetterboxMeta): Detection {
|
||||
return Detection(
|
||||
cellClass = cellClass,
|
||||
confidence = confidence,
|
||||
centerX = (centerX - meta.padX) / meta.gain,
|
||||
centerY = (centerY - meta.padY) / meta.gain,
|
||||
width = width / meta.gain,
|
||||
height = height / meta.gain
|
||||
)
|
||||
}
|
||||
|
||||
private fun iou(
|
||||
a: Detection,
|
||||
b: Detection
|
||||
): Float {
|
||||
val interH = ((a.height + b.height) / 2f - abs(a.centerY - b.centerY))
|
||||
.coerceAtLeast(0f)
|
||||
|
||||
val interW = ((a.width + b.width) / 2f - abs(a.centerX - b.centerX))
|
||||
.coerceAtLeast(0f)
|
||||
|
||||
val interArea = interW * interH
|
||||
|
||||
val areaA = a.width * a.height
|
||||
val areaB = b.width * b.height
|
||||
|
||||
val union = (areaA + areaB - interArea)
|
||||
.coerceAtLeast(0.0001f)
|
||||
|
||||
return interArea / union
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.postprocess
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.Detection
|
||||
|
||||
internal data class PostprocessOutput(
|
||||
val detections: List<Detection>
|
||||
)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.preprocess
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.gray
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.rgb
|
||||
|
||||
fun brightnessContrast(imageBuffer: ImageBuffer): ImageBuffer {
|
||||
val luminance = IntArray(imageBuffer.pixels.size) { gray(imageBuffer.pixels[it]) }
|
||||
val min = luminance.minOrNull() ?: return imageBuffer
|
||||
val max = luminance.maxOrNull() ?: return imageBuffer
|
||||
if (max == min) return imageBuffer
|
||||
|
||||
val result = IntArray(imageBuffer.pixels.size)
|
||||
for (i in imageBuffer.pixels.indices) {
|
||||
val pixel = imageBuffer.pixels[i]
|
||||
val oldLum = luminance[i].coerceAtLeast(1)
|
||||
val newLum = ((luminance[i] - min) * 255 / (max - min)).coerceIn(0, 255)
|
||||
val scale = newLum.toDouble() / oldLum
|
||||
|
||||
result[i] = rgb(
|
||||
((pixel ushr 16 and 0xFF) * scale).toInt().coerceIn(0, 255),
|
||||
((pixel ushr 8 and 0xFF) * scale).toInt().coerceIn(0, 255),
|
||||
((pixel and 0xFF) * scale).toInt().coerceIn(0, 255)
|
||||
)
|
||||
}
|
||||
|
||||
return imageBuffer.copy(pixels = result)
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.preprocess
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.MODEL_INPUT_HEIGHT
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.MODEL_INPUT_WIDTH
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.blue
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.green
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.red
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.rgb
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.data.LetterboxMeta
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.PreprocessVariant
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
internal class PreprocessHandler {
|
||||
operator fun invoke(
|
||||
imageBuffer: ImageBuffer,
|
||||
preprocessVariant: PreprocessVariant
|
||||
): PreprocessOutput {
|
||||
val buffer = when (preprocessVariant) {
|
||||
PreprocessVariant.Default -> imageBuffer
|
||||
PreprocessVariant.BrightnessContrast -> brightnessContrast(imageBuffer)
|
||||
}
|
||||
|
||||
val letterboxed = letterbox(
|
||||
image = buffer,
|
||||
targetWidth = MODEL_INPUT_WIDTH,
|
||||
targetHeight = MODEL_INPUT_HEIGHT
|
||||
)
|
||||
|
||||
return letterboxed
|
||||
}
|
||||
|
||||
private fun letterbox(
|
||||
image: ImageBuffer,
|
||||
targetWidth: Int,
|
||||
targetHeight: Int,
|
||||
fillRgb: Int = GRAY
|
||||
): PreprocessOutput = with(image) {
|
||||
val gain = min(
|
||||
MODEL_INPUT_WIDTH.toFloat() / width.toFloat(),
|
||||
MODEL_INPUT_HEIGHT.toFloat() / height.toFloat()
|
||||
)
|
||||
|
||||
val resizedWidth = (width * gain).roundToInt().coerceAtLeast(1)
|
||||
val resizedHeight = (height * gain).roundToInt().coerceAtLeast(1)
|
||||
|
||||
val resized = resizeBilinear(
|
||||
image = image,
|
||||
targetWidth = resizedWidth,
|
||||
targetHeight = resizedHeight
|
||||
)
|
||||
|
||||
val padX = (targetWidth - resizedWidth) / 2f
|
||||
val padY = (targetHeight - resizedHeight) / 2f
|
||||
|
||||
val left = (padX - 0.1f).roundToInt()
|
||||
val top = (padY - 0.1f).roundToInt()
|
||||
|
||||
val out = IntArray(targetWidth * targetHeight) { fillRgb }
|
||||
|
||||
for (y in 0 until resizedHeight) {
|
||||
for (x in 0 until resizedWidth) {
|
||||
val dstX = left + x
|
||||
val dstY = top + y
|
||||
|
||||
if (dstX in 0 until targetWidth && dstY in 0 until targetHeight) {
|
||||
out[dstY * targetWidth + dstX] = resized[x, y]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PreprocessOutput(
|
||||
imageBuffer = ImageBuffer(
|
||||
width = targetWidth,
|
||||
height = targetHeight,
|
||||
pixels = out
|
||||
),
|
||||
meta = LetterboxMeta(
|
||||
gain = gain,
|
||||
padX = padX,
|
||||
padY = padY,
|
||||
left = left,
|
||||
top = top
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun resizeBilinear(
|
||||
image: ImageBuffer,
|
||||
targetWidth: Int,
|
||||
targetHeight: Int
|
||||
): ImageBuffer {
|
||||
if (image.width == targetWidth && image.height == targetHeight) {
|
||||
return image
|
||||
}
|
||||
|
||||
val out = IntArray(targetWidth * targetHeight)
|
||||
|
||||
val xRatio = if (targetWidth > 1) {
|
||||
(image.width - 1).toFloat() / (targetWidth - 1).toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
val yRatio = if (targetHeight > 1) {
|
||||
(image.height - 1).toFloat() / (targetHeight - 1).toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
for (y in 0 until targetHeight) {
|
||||
val srcY = y * yRatio
|
||||
val y0 = srcY.toInt()
|
||||
val y1 = min(y0 + 1, image.height - 1)
|
||||
val yWeight = srcY - y0
|
||||
|
||||
for (x in 0 until targetWidth) {
|
||||
val srcX = x * xRatio
|
||||
val x0 = srcX.toInt()
|
||||
val x1 = min(x0 + 1, image.width - 1)
|
||||
val xWeight = srcX - x0
|
||||
|
||||
val p00 = image[x0, y0]
|
||||
val p10 = image[x1, y0]
|
||||
val p01 = image[x0, y1]
|
||||
val p11 = image[x1, y1]
|
||||
|
||||
val r = bilinear(red(p00), red(p10), red(p01), red(p11), xWeight, yWeight)
|
||||
val g = bilinear(green(p00), green(p10), green(p01), green(p11), xWeight, yWeight)
|
||||
val b = bilinear(blue(p00), blue(p10), blue(p01), blue(p11), xWeight, yWeight)
|
||||
|
||||
out[y * targetWidth + x] = rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
return ImageBuffer(
|
||||
width = targetWidth,
|
||||
height = targetHeight,
|
||||
pixels = out
|
||||
)
|
||||
}
|
||||
|
||||
private fun bilinear(
|
||||
v00: Int,
|
||||
v10: Int,
|
||||
v01: Int,
|
||||
v11: Int,
|
||||
xWeight: Float,
|
||||
yWeight: Float
|
||||
): Int {
|
||||
val top = v00 * (1f - xWeight) + v10 * xWeight
|
||||
val bottom = v01 * (1f - xWeight) + v11 * xWeight
|
||||
return (top * (1f - yWeight) + bottom * yWeight)
|
||||
.roundToInt()
|
||||
.coerceIn(0, 255)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val GRAY: Int = rgb(114, 114, 114)
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.preprocess
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.data.LetterboxMeta
|
||||
|
||||
internal data class PreprocessOutput(
|
||||
val imageBuffer: ImageBuffer,
|
||||
val meta: LetterboxMeta
|
||||
)
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.roi
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.RoiVariant
|
||||
|
||||
internal class RoiHandler {
|
||||
operator fun invoke(
|
||||
imageBuffer: ImageBuffer,
|
||||
roiVariant: RoiVariant
|
||||
): RoiOutput {
|
||||
val image = when (roiVariant) {
|
||||
RoiVariant.ThresholdMorph -> thresholdMorph(imageBuffer)
|
||||
RoiVariant.ThresholdFilter -> thresholdFilter(imageBuffer)
|
||||
RoiVariant.None -> imageBuffer
|
||||
}
|
||||
return RoiOutput(image)
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.roi
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
|
||||
internal data class RoiOutput(
|
||||
val imageBuffer: ImageBuffer
|
||||
)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.roi
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.BACKGROUND_COLOR
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.gray
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.otsuThreshold
|
||||
|
||||
|
||||
internal fun thresholdFilter(imageBuffer: ImageBuffer): ImageBuffer {
|
||||
val threshold = otsuThreshold(imageBuffer)
|
||||
val result = IntArray(imageBuffer.pixels.size)
|
||||
|
||||
for (i in imageBuffer.pixels.indices) {
|
||||
val pixel = imageBuffer.pixels[i]
|
||||
result[i] = if (gray(pixel) <= threshold) pixel else BACKGROUND_COLOR
|
||||
}
|
||||
|
||||
return imageBuffer.copy(pixels = result)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.roi
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.BACKGROUND_COLOR
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.close
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.gray
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.open
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.otsuThreshold
|
||||
|
||||
internal fun thresholdMorph(imageBuffer: ImageBuffer): ImageBuffer {
|
||||
val threshold = otsuThreshold(imageBuffer)
|
||||
val mask = BooleanArray(imageBuffer.pixels.size) {
|
||||
gray(imageBuffer.pixels[it]) <= threshold
|
||||
}
|
||||
val morphedMask = close(
|
||||
open(mask, imageBuffer.width, imageBuffer.height),
|
||||
imageBuffer.width,
|
||||
imageBuffer.height
|
||||
)
|
||||
val result = IntArray(imageBuffer.pixels.size)
|
||||
|
||||
for (i in imageBuffer.pixels.indices) {
|
||||
val pixel = imageBuffer.pixels[i]
|
||||
result[i] = if (morphedMask[i]) pixel else BACKGROUND_COLOR
|
||||
}
|
||||
|
||||
return imageBuffer.copy(pixels = result)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.tensor
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.blue
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.green
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.red
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
|
||||
internal class TensorHandler {
|
||||
operator fun invoke(imageBuffer: ImageBuffer): FloatArray = with(imageBuffer) {
|
||||
val channelSize = width * height
|
||||
val tensor = FloatArray(3 * channelSize)
|
||||
|
||||
for (i in pixels.indices) {
|
||||
val pixel = pixels[i]
|
||||
|
||||
tensor[i] = red(pixel) / 255f
|
||||
tensor[channelSize + i] = green(pixel) / 255f
|
||||
tensor[channelSize * 2 + i] = blue(pixel) / 255f
|
||||
}
|
||||
|
||||
return tensor
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl
|
||||
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.CPointer
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.ObjCObjectVar
|
||||
import kotlinx.cinterop.alloc
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.ptr
|
||||
import kotlinx.cinterop.value
|
||||
import platform.Foundation.NSError
|
||||
|
||||
|
||||
@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class)
|
||||
internal inline fun <T> handleThrowingError(
|
||||
block: (CPointer<ObjCObjectVar<NSError?>>) -> T?
|
||||
): T = memScoped {
|
||||
val error = alloc<ObjCObjectVar<NSError?>>()
|
||||
error.value = null
|
||||
|
||||
val result = block(error.ptr)
|
||||
result ?: throw IllegalStateException(
|
||||
"ONNX Runtime failed: ${error.value?.localizedDescription}"
|
||||
)
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
@file:OptIn(ExperimentalForeignApi::class)
|
||||
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl
|
||||
|
||||
import cocoapods.onnxruntime_objc.ORTEnv
|
||||
import cocoapods.onnxruntime_objc.ORTGraphOptimizationLevel
|
||||
import cocoapods.onnxruntime_objc.ORTLoggingLevel
|
||||
import cocoapods.onnxruntime_objc.ORTSession
|
||||
import cocoapods.onnxruntime_objc.ORTSessionOptions
|
||||
import cocoapods.onnxruntime_objc.ORTTensorElementDataType
|
||||
import cocoapods.onnxruntime_objc.ORTValue
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.convert
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.Foundation.NSMutableData
|
||||
import platform.Foundation.NSNumber
|
||||
import platform.Foundation.dataWithLength
|
||||
import platform.Foundation.numberWithLongLong
|
||||
import platform.posix.memcpy
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.MODEL_INPUT_HEIGHT
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.core.MODEL_INPUT_WIDTH
|
||||
|
||||
internal object OrtRuntime {
|
||||
private val environment: ORTEnv = handleThrowingError { error ->
|
||||
ORTEnv(
|
||||
loggingLevel = ORTLoggingLevel.ORTLoggingLevelWarning,
|
||||
error = error
|
||||
)
|
||||
}
|
||||
|
||||
private val options: ORTSessionOptions = handleThrowingError { error ->
|
||||
ORTSessionOptions(error = error).apply {
|
||||
setGraphOptimizationLevel(
|
||||
ORTGraphOptimizationLevel.ORTGraphOptimizationLevelAll,
|
||||
error = error
|
||||
)
|
||||
setGraphOptimizationLevel(
|
||||
ORTGraphOptimizationLevel.ORTGraphOptimizationLevelAll,
|
||||
error = error
|
||||
)
|
||||
addConfigEntryWithKey(
|
||||
"session.load_model_format",
|
||||
value = "ORT",
|
||||
error = error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createSession(modelPath: String): ORTSession =
|
||||
handleThrowingError { error ->
|
||||
ORTSession(
|
||||
env = environment,
|
||||
modelPath = modelPath,
|
||||
sessionOptions = options,
|
||||
error = error
|
||||
)
|
||||
}
|
||||
|
||||
fun createTensor(tensorData: FloatArray): ORTValue =
|
||||
handleThrowingError { error ->
|
||||
ORTValue(
|
||||
tensorData = tensorData.toNSMutableData(),
|
||||
elementType = ORTTensorElementDataType.ORTTensorElementDataTypeFloat,
|
||||
shape = listOf(
|
||||
NSNumber.numberWithLongLong(1),
|
||||
NSNumber.numberWithLongLong(3),
|
||||
NSNumber.numberWithLongLong(MODEL_INPUT_WIDTH.toLong()),
|
||||
NSNumber.numberWithLongLong(MODEL_INPUT_HEIGHT.toLong())
|
||||
),
|
||||
error = error
|
||||
)
|
||||
}
|
||||
|
||||
private fun FloatArray.toNSMutableData(): NSMutableData {
|
||||
val byteSize = size * Float.SIZE_BYTES
|
||||
|
||||
val data = NSMutableData.dataWithLength(byteSize.convert())
|
||||
?: error("Failed to allocate input tensor data: $byteSize bytes")
|
||||
|
||||
usePinned { pinned ->
|
||||
memcpy(
|
||||
data.mutableBytes,
|
||||
pinned.addressOf(0),
|
||||
byteSize.convert()
|
||||
)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
@file:OptIn(ExperimentalForeignApi::class)
|
||||
|
||||
package ru.tanexc.hemoanalysis.tool.analysis.impl.inference
|
||||
|
||||
import cocoapods.onnxruntime_objc.ORTSession
|
||||
import cocoapods.onnxruntime_objc.ORTValue
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.FloatVar
|
||||
import kotlinx.cinterop.get
|
||||
import kotlinx.cinterop.reinterpret
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import platform.Foundation.NSNumber
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetModelPathUseCase
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.OrtRuntime
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.impl.handleThrowingError
|
||||
|
||||
internal actual class InferenceHandler : KoinComponent {
|
||||
private val getModelPathUseCase: GetModelPathUseCase by inject()
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
private lateinit var session: ORTSession
|
||||
|
||||
init {
|
||||
initSession()
|
||||
}
|
||||
|
||||
private fun initSession() {
|
||||
val isLatestModel = modelRepository.getCurrentModelInfo()?.isLatest?: false
|
||||
if (!isLatestModel || !::session.isInitialized) {
|
||||
getModelPathUseCase()?.let {
|
||||
session = OrtRuntime.createSession(modelPath = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual operator fun invoke(tensorImage: FloatArray): InferenceOutput {
|
||||
initSession()
|
||||
if (!::session.isInitialized) error("Model does not exists")
|
||||
|
||||
val inputTensor = OrtRuntime.createTensor(tensorImage)
|
||||
|
||||
val outputName = handleThrowingError { error ->
|
||||
session.outputNamesWithError(error)?.first()
|
||||
}
|
||||
|
||||
val outputs = handleThrowingError { error ->
|
||||
session.runWithInputs(
|
||||
mapOf(session.inputNamesWithError(error)?.first() to inputTensor),
|
||||
outputNames = setOf(outputName),
|
||||
runOptions = null,
|
||||
error = error
|
||||
)
|
||||
}
|
||||
|
||||
val outputValue = outputs[outputName] as ORTValue
|
||||
return outputValue.asInferenceOutput()
|
||||
}
|
||||
|
||||
private fun ORTValue.asInferenceOutput(): InferenceOutput {
|
||||
val tensorInfo = handleThrowingError { error ->
|
||||
tensorTypeAndShapeInfoWithError(error)
|
||||
}
|
||||
|
||||
val outputShape = tensorInfo.shape.map { number ->
|
||||
(number as NSNumber).longLongValue.toInt()
|
||||
}
|
||||
|
||||
val channelCount = outputShape[1]
|
||||
val candidateCount = outputShape[2]
|
||||
|
||||
val tensorData = handleThrowingError { error ->
|
||||
tensorDataWithError(error)
|
||||
}
|
||||
|
||||
val rawFloats = tensorData.bytes!!.reinterpret<FloatVar>()
|
||||
var index = 0
|
||||
|
||||
val outputData = Array(1) {
|
||||
Array(channelCount) {
|
||||
FloatArray(candidateCount)
|
||||
}
|
||||
}
|
||||
|
||||
for (channel in 0 until channelCount) {
|
||||
for (candidate in 0 until candidateCount) {
|
||||
outputData[0][channel][candidate] = rawFloats[index++]
|
||||
}
|
||||
}
|
||||
|
||||
return InferenceOutput(
|
||||
channelCount = channelCount,
|
||||
candidateCount = candidateCount,
|
||||
outputData = outputData
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user