video commit first feedbacks

This commit is contained in:
SaiD 2025-12-05 21:20:13 +05:30
parent 4ba7aa398d
commit bc2e32dc75
5 changed files with 77 additions and 54 deletions

View File

@ -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

View File

@ -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
}
}

View File

@ -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
)

View File

@ -19,4 +19,5 @@ object Constants {
val VIDEO_SILHOETTE_FRAME_HEIGHT = 300.dp
const val JACCARD_THRESHOLD = 85
const val MODEL_HEIGHT = 320F
}