video commit first feedbacks
This commit is contained in:
parent
4ba7aa398d
commit
bc2e32dc75
|
|
@ -0,0 +1,4 @@
|
|||
kotlin version: 2.0.21
|
||||
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||
1. Kotlin compile daemon is ready
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.example.livingai.domain.ml
|
||||
|
||||
import android.graphics.RectF
|
||||
import android.util.Log
|
||||
import com.example.livingai.utils.Constants
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.sin
|
||||
|
|
@ -9,24 +11,16 @@ import kotlin.math.sin
|
|||
// CONFIG CLASS
|
||||
// ------------------------------------------------------------
|
||||
data class AnalyzerThresholds(
|
||||
// Size thresholds
|
||||
val minCoverage: Float = 0.15f,
|
||||
val maxCoverage: Float = 0.70f,
|
||||
val toleranceRatio: Float = 0.02f,
|
||||
|
||||
// Vertical centering thresholds
|
||||
val topMarginRatio: Float = 0.05f,
|
||||
val bottomMarginRatio: Float = 0.05f,
|
||||
val centerToleranceRatio: Float = 0.10f,
|
||||
// Height estimation
|
||||
val minTargetHeightMeters: Float = 0.60f,
|
||||
val maxTargetHeightMeters: Float = 0.70f,
|
||||
|
||||
// Height estimation thresholds
|
||||
val targetHeightMeters: Float = 0.70f,
|
||||
val heightToleranceMeters: Float = 0.05f, // ±5 cm allowed
|
||||
|
||||
// Real height of subject (for estimating camera distance)
|
||||
val subjectRealHeightMeters: Float = 1.70f // default human height or silhouette height
|
||||
// Real physical height of subject
|
||||
val subjectRealHeightMeters: Float = 1.55f
|
||||
)
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// STATES
|
||||
// ------------------------------------------------------------
|
||||
|
|
@ -41,21 +35,20 @@ sealed class FeedbackState(val message: String) {
|
|||
object Optimal : FeedbackState("Hold still")
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// ANALYZER
|
||||
// ANALYZER INTERFACE
|
||||
// ------------------------------------------------------------
|
||||
interface FeedbackAnalyzer {
|
||||
fun analyze(
|
||||
detection: ObjectDetector.DetectionResult?,
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
tiltDegrees: Float, // phone pitch for height estimation
|
||||
focalLengthPx: Float // camera intrinsics for height estimation
|
||||
screenHeight: Int,
|
||||
tiltDegrees: Float,
|
||||
focalLengthPx: Float
|
||||
): FeedbackState
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// IMPLEMENTATION
|
||||
// ------------------------------------------------------------
|
||||
|
|
@ -67,6 +60,7 @@ class FeedbackAnalyzerImpl(
|
|||
detection: ObjectDetector.DetectionResult?,
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
screenHeight: Int,
|
||||
tiltDegrees: Float,
|
||||
focalLengthPx: Float
|
||||
): FeedbackState {
|
||||
|
|
@ -74,77 +68,97 @@ class FeedbackAnalyzerImpl(
|
|||
if (detection == null) return FeedbackState.Searching
|
||||
if (frameWidth <= 0 || frameHeight <= 0) return FeedbackState.Idle
|
||||
|
||||
val box = detection.boundingBox
|
||||
|
||||
val pc = Precomputed(box, frameWidth, frameHeight, thresholds)
|
||||
val pc = Precomputed(
|
||||
detection.boundingBox,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
screenHeight,
|
||||
thresholds
|
||||
)
|
||||
|
||||
val cameraHeight = estimateCameraHeight(pc, tiltDegrees, focalLengthPx)
|
||||
|
||||
Log.d("FeedbackAnalyzer", "Camera Height: $cameraHeight, frame height: ${pc.frameHeight}, " +
|
||||
"frame top: ${pc.frameTop}, frame bottom: ${pc.frameBottom}," +
|
||||
" detection top: ${pc.detectionTop}, detection bottom: ${pc.detectionBottom}, " +
|
||||
"isTopInside: ${pc.isTopInside}, isBottomInside: ${pc.isBottomInside}," +
|
||||
" detection height: ${pc.detectionHeight}, tolerance: ${pc.tolerance}")
|
||||
|
||||
return when {
|
||||
isTooFar(pc) -> FeedbackState.TooFar
|
||||
// ORDER MATTERS — evaluate alignment first
|
||||
isTooHigh(pc) -> FeedbackState.TooHigh
|
||||
isTooLow(pc) -> FeedbackState.TooLow
|
||||
isTooClose(pc) -> FeedbackState.TooClose
|
||||
isNotCentered(pc) -> FeedbackState.NotCentered
|
||||
isTooFar(pc) -> FeedbackState.TooFar
|
||||
|
||||
// Height estimation last
|
||||
isHeightTooLow(cameraHeight) -> FeedbackState.TooLow
|
||||
isHeightTooHigh(cameraHeight) -> FeedbackState.TooHigh
|
||||
|
||||
else -> FeedbackState.Optimal
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTooFar(pc: Precomputed) =
|
||||
pc.verticalCoverage < thresholds.minCoverage
|
||||
private fun isTooLow(pc: Precomputed): Boolean =
|
||||
pc.isBottomInside && !pc.isTopInside && ((pc.frameTop - pc.detectionTop) > pc.tolerance)
|
||||
|
||||
private fun isTooClose(pc: Precomputed) =
|
||||
pc.verticalCoverage > thresholds.maxCoverage
|
||||
private fun isTooHigh(pc: Precomputed): Boolean =
|
||||
!pc.isBottomInside && pc.isTopInside && ((pc.detectionBottom - pc.frameBottom) > pc.tolerance)
|
||||
|
||||
private fun isNotCentered(pc: Precomputed) =
|
||||
!pc.topWithinMargin || !pc.bottomWithinMargin || !pc.centeredVertically
|
||||
// OBJECT TOO CLOSE (bigger than allowed)
|
||||
private fun isTooClose(pc: Precomputed): Boolean =
|
||||
!pc.isTopInside && !pc.isBottomInside && ((pc.detectionHeight - pc.frameHeight) > pc.tolerance)
|
||||
|
||||
private fun isHeightTooLow(camHeight: Float) =
|
||||
camHeight < (thresholds.targetHeightMeters - thresholds.heightToleranceMeters)
|
||||
// OBJECT TOO FAR (too small)
|
||||
private fun isTooFar(pc: Precomputed): Boolean =
|
||||
pc.isTopInside && pc.isBottomInside &&
|
||||
((pc.frameHeight - pc.detectionHeight) > pc.tolerance)
|
||||
|
||||
private fun isHeightTooHigh(camHeight: Float) =
|
||||
camHeight > (thresholds.targetHeightMeters + thresholds.heightToleranceMeters)
|
||||
private fun isHeightTooLow(heightMeters: Float): Boolean =
|
||||
heightMeters > 0 &&
|
||||
(thresholds.minTargetHeightMeters > heightMeters)
|
||||
|
||||
private fun isHeightTooHigh(heightMeters: Float): Boolean =
|
||||
heightMeters > (thresholds.maxTargetHeightMeters)
|
||||
|
||||
private fun estimateCameraHeight(
|
||||
pc: Precomputed,
|
||||
tiltDegrees: Float,
|
||||
focalLengthPx: Float
|
||||
): Float {
|
||||
|
||||
val tiltRad = Math.toRadians(tiltDegrees.toDouble())
|
||||
|
||||
val realHeight = thresholds.subjectRealHeightMeters
|
||||
val pixelHeight = pc.heightPx
|
||||
val pixelHeight = pc.detectionHeight
|
||||
|
||||
if (pixelHeight <= 0f || focalLengthPx <= 0f) return -1f
|
||||
|
||||
val distance = (realHeight * focalLengthPx) / pixelHeight
|
||||
|
||||
val height = distance * sin(tiltRad)
|
||||
|
||||
return height.toFloat() // in meters
|
||||
return (distance * sin(tiltRad)).toFloat()
|
||||
}
|
||||
|
||||
private data class Precomputed(
|
||||
val box: RectF,
|
||||
val frameWidth: Int,
|
||||
val frameHeight: Int,
|
||||
val screenHeight: Int,
|
||||
val t: AnalyzerThresholds
|
||||
) {
|
||||
val top = box.top
|
||||
val bottom = box.bottom
|
||||
val heightPx = max(0f, bottom - top)
|
||||
val verticalCoverage: Float = heightPx / frameHeight
|
||||
private val modelFrameHeight = Constants.MODEL_HEIGHT
|
||||
private val scaleSlip = ((screenHeight - modelFrameHeight) * screenHeight) / (2F * modelFrameHeight)
|
||||
val detectionTop = (box.top * screenHeight / modelFrameHeight) - scaleSlip
|
||||
val detectionBottom = (box.bottom * screenHeight / modelFrameHeight) - scaleSlip
|
||||
val detectionHeight = max(0f, detectionBottom - detectionTop)
|
||||
|
||||
private val topMarginPx = frameHeight * t.topMarginRatio
|
||||
private val bottomMarginPx = frameHeight * t.bottomMarginRatio
|
||||
private val centerTolerancePx = frameHeight * t.centerToleranceRatio
|
||||
// Frame centered vertically
|
||||
val frameTop = (screenHeight - frameHeight) / 2f
|
||||
val frameBottom = frameTop + frameHeight
|
||||
|
||||
val topWithinMargin = top >= topMarginPx
|
||||
val bottomWithinMargin = bottom <= (frameHeight - bottomMarginPx)
|
||||
val tolerance = t.toleranceRatio * screenHeight
|
||||
|
||||
// Inside checks with tolerance
|
||||
val isTopInside = detectionTop >= frameTop
|
||||
val isBottomInside = detectionBottom <= frameBottom
|
||||
|
||||
val centerY = box.centerY()
|
||||
val frameCenterY = frameHeight / 2f
|
||||
val centeredVertically = abs(centerY - frameCenterY) <= centerTolerancePx
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.example.livingai.pages.camera
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
|
|
@ -8,6 +9,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.example.livingai.domain.ml.FeedbackAnalyzer
|
||||
import com.example.livingai.domain.ml.FeedbackState
|
||||
import com.example.livingai.domain.ml.ObjectDetector
|
||||
import com.example.livingai.utils.Constants
|
||||
import com.example.livingai.utils.TiltSensorManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -44,6 +46,7 @@ class VideoViewModel(
|
|||
detection = cow,
|
||||
frameWidth = state.value.frameWidth,
|
||||
frameHeight = state.value.frameHeight,
|
||||
screenHeight = state.value.screenHeight,
|
||||
tiltDegrees = state.value.tilt,
|
||||
focalLengthPx = state.value.focalLength
|
||||
)
|
||||
|
|
@ -78,10 +81,10 @@ class VideoViewModel(
|
|||
_state.value = _state.value.copy(recordedVideoUri = null)
|
||||
}
|
||||
is VideoEvent.FrameReceived -> {
|
||||
// Update frame dimensions and focal length, but let TiltSensorManager handle tilt
|
||||
_state.value = _state.value.copy(
|
||||
frameWidth = event.bitmap.width,
|
||||
frameHeight = event.bitmap.height,
|
||||
frameHeight = Constants.VIDEO_SILHOETTE_FRAME_HEIGHT.value.toInt(),
|
||||
screenHeight = event.bitmap.height,
|
||||
focalLength = event.focalLength
|
||||
)
|
||||
objectDetector.detect(event.bitmap, event.rotation)
|
||||
|
|
@ -97,6 +100,7 @@ data class VideoState(
|
|||
val feedback: FeedbackState = FeedbackState.Idle,
|
||||
val frameWidth: Int = 0,
|
||||
val frameHeight: Int = 0,
|
||||
val screenHeight: Int = 0,
|
||||
val tilt: Float = 0f,
|
||||
val focalLength: Float = 0f
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ object Constants {
|
|||
|
||||
val VIDEO_SILHOETTE_FRAME_HEIGHT = 300.dp
|
||||
const val JACCARD_THRESHOLD = 85
|
||||
const val MODEL_HEIGHT = 320F
|
||||
}
|
||||
Loading…
Reference in New Issue