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