This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: draw text labels in drawobjects

+206 -22
+61
posedetection/src/androidMain/kotlin/com/performancecoachlab/posedetection/camera/LabelTextAndroid.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + import androidx.compose.ui.graphics.drawscope.DrawScope 4 + import androidx.compose.ui.graphics.nativeCanvas 5 + import androidx.compose.ui.text.TextMeasurer 6 + 7 + actual fun DrawScope.drawLabelTextPlatform( 8 + drawableObject: DrawableObject, 9 + textMeasurer: TextMeasurer? 10 + ) { 11 + val label = drawableObject.obj.labels.firstOrNull()?.text ?: "Object" 12 + if (label.isEmpty()) return 13 + 14 + val box = drawableObject.obj.boundingBox 15 + val boxLeft = box.left 16 + val boxTop = box.top 17 + val boxWidth = box.width 18 + val boxHeight = box.height 19 + 20 + if (boxWidth <= 0f || boxHeight <= 0f) return 21 + 22 + // Padding inside the box so text doesn't touch the edges 23 + val paddingX = boxWidth * 0.08f 24 + val paddingY = boxHeight * 0.12f 25 + 26 + val availableWidth = (boxWidth - 2 * paddingX).coerceAtLeast(1f) 27 + val availableHeight = (boxHeight - 2 * paddingY).coerceAtLeast(1f) 28 + 29 + // Start with a reasonable text size relative to box height 30 + val baseTextSizePx = (boxHeight * 0.5f).coerceAtLeast(8f) 31 + 32 + val paint = android.graphics.Paint().apply { 33 + isAntiAlias = true 34 + color = android.graphics.Color.WHITE 35 + textAlign = android.graphics.Paint.Align.LEFT 36 + textSize = baseTextSizePx 37 + } 38 + 39 + val textBounds = android.graphics.Rect() 40 + paint.getTextBounds(label, 0, label.length, textBounds) 41 + var textWidth = textBounds.width().toFloat().coerceAtLeast(1f) 42 + var textHeight = textBounds.height().toFloat().coerceAtLeast(1f) 43 + 44 + // Compute scale factors to fit text within available area 45 + val scaleX = availableWidth / textWidth 46 + val scaleY = availableHeight / textHeight 47 + val scale = minOf(scaleX, scaleY, 1f) // never scale up, only down 48 + 49 + if (scale < 1f) { 50 + paint.textSize = baseTextSizePx * scale 51 + paint.getTextBounds(label, 0, label.length, textBounds) 52 + textWidth = textBounds.width().toFloat() 53 + textHeight = textBounds.height().toFloat() 54 + } 55 + 56 + // Center text within the box, respecting padding 57 + val x = boxLeft + paddingX + (availableWidth - textWidth) / 2f 58 + val y = boxTop + paddingY + (availableHeight + textHeight) / 2f 59 + 60 + drawContext.canvas.nativeCanvas.drawText(label, x, y, paint) 61 + }
+3 -2
posedetection/src/androidMain/kotlin/com/performancecoachlab/posedetection/camera/Utils.android.kt
··· 13 13 import com.performancecoachlab.posedetection.recording.FrameSize 14 14 import com.performancecoachlab.posedetection.skeleton.Skeleton 15 15 import org.tensorflow.lite.support.image.TensorImage 16 + import kotlin.math.absoluteValue 16 17 17 18 actual enum class PlatformType { 18 19 ANDROID, IOS; ··· 195 196 ) 196 197 }, 197 198 frameSize = FrameSize( 198 - width = width, 199 - height = height 199 + width = width.absoluteValue, 200 + height = height.absoluteValue 200 201 ) 201 202 ) 202 203 } ?: emptyList()
+1 -1
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/camera/CameraView.kt
··· 46 46 ) 47 47 48 48 enum class DrawableShape { 49 - OVAL,RECTANGLE 49 + OVAL,RECTANGLE,LABEL 50 50 } 51 51 52 52 data class CameraViewData(
+44 -2
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/camera/Utils.kt
··· 1 1 package com.performancecoachlab.posedetection.camera 2 2 3 + import androidx.compose.ui.geometry.Rect 3 4 import androidx.compose.ui.geometry.Size 4 5 import androidx.compose.ui.graphics.BlendMode 5 6 import androidx.compose.ui.graphics.Brush ··· 10 11 import androidx.compose.ui.graphics.PaintingStyle.Companion.Fill 11 12 import androidx.compose.ui.graphics.PaintingStyle.Companion.Stroke 12 13 import androidx.compose.ui.graphics.drawscope.CanvasDrawScope 14 + import androidx.compose.ui.graphics.drawscope.DrawScope 13 15 import androidx.compose.ui.graphics.drawscope.Stroke 14 16 import androidx.compose.ui.graphics.rotate 17 + import androidx.compose.ui.text.TextMeasurer 15 18 import androidx.compose.ui.unit.Density 16 19 import androidx.compose.ui.unit.LayoutDirection 20 + import co.touchlab.kermit.Logger 17 21 import com.performancecoachlab.posedetection.recording.AnalysisResult 18 22 import com.performancecoachlab.posedetection.skeleton.Skeleton 19 23 ··· 102 106 } 103 107 104 108 fun ImageBitmap.drawResults( 105 - skeleton: Skeleton?, drawableObjects: List<DrawableObject> 109 + skeleton: Skeleton?, drawableObjects: List<DrawableObject>, 110 + textMeasurer: TextMeasurer? = null 106 111 ): ImageBitmap { 107 - also { 112 + also { it -> 108 113 val bitmap = ImageBitmap(it.width, it.height) 109 114 val canvas = Canvas(bitmap) 110 115 val drawScope = CanvasDrawScope() 111 116 val size = Size(it.width.toFloat(), it.height.toFloat()) 117 + Logger.d{"Size ${size.width} x ${size.height}"} 112 118 113 119 // Calculate scaling factor based on skeleton size 114 120 val skeletonSize = skeleton?.joints()?.let { joints -> ··· 155 161 ), 156 162 style = drawableObject.style 157 163 ) 164 + 165 + DrawableShape.LABEL -> { 166 + // Draw the label background 167 + drawRect( 168 + color = drawableObject.colour, 169 + topLeft = androidx.compose.ui.geometry.Offset( 170 + drawableObject.obj.boundingBox.left, 171 + drawableObject.obj.boundingBox.top 172 + ), 173 + size = Size( 174 + drawableObject.obj.boundingBox.width, 175 + drawableObject.obj.boundingBox.height 176 + ), 177 + style = drawableObject.style 178 + ) 179 + // Defer actual text rendering to platform-specific implementation 180 + drawLabelTextPlatform( 181 + drawableObject = drawableObject, 182 + textMeasurer = textMeasurer 183 + ) 184 + } 158 185 } 159 186 } 160 187 skeleton?.apply { ··· 296 323 return bitmap 297 324 } 298 325 } 326 + 327 + fun Rect.normalize(): Rect { 328 + return Rect( 329 + left = minOf(this.left, this.right), 330 + top = minOf(this.top, this.bottom), 331 + right = maxOf(this.left, this.right), 332 + bottom = maxOf(this.top, this.bottom) 333 + ) 334 + } 335 + 336 + // Platform-specific label text drawing 337 + expect fun DrawScope.drawLabelTextPlatform( 338 + drawableObject: DrawableObject, 339 + textMeasurer: TextMeasurer? 340 + ) 299 341 300 342 expect enum class PlatformType { 301 343 ANDROID, IOS;
+12 -3
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraEngine.kt
··· 100 100 import platform.UIKit.UIInterfaceOrientationPortrait 101 101 import platform.UIKit.UIInterfaceOrientationPortraitUpsideDown 102 102 import platform.UIKit.UIViewControllerTransitionCoordinatorProtocol 103 + import kotlin.math.absoluteValue 103 104 104 105 fun interfaceOrientationToVideoOrientation(): AVCaptureVideoOrientation { 105 106 val orientation = UIApplication.sharedApplication.keyWindow?.windowScene?.interfaceOrientation ··· 117 118 class CameraEngine : UIViewController(null, null) { 118 119 val cameraController = CameraController() 119 120 private var onVideoSaved: ((String) -> Unit)? = null 121 + 122 + private var textMeasurer: androidx.compose.ui.text.TextMeasurer? = null 120 123 121 124 private val memoryManager = MemoryManager 122 125 ··· 256 259 cameraController.onVideoSaved = callback 257 260 } 258 261 262 + fun setTextMeasurer(textMeasurer: androidx.compose.ui.text.TextMeasurer) { 263 + cameraController.textMeasurer = textMeasurer 264 + } 265 + 259 266 fun setDrawOptions( 260 267 drawSkeleton: Boolean, drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)? 261 268 ) { ··· 291 298 var onVideoSaved: ((String) -> Unit)? = null 292 299 var drawSkeleton: Boolean = true 293 300 var drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)? = null 301 + var textMeasurer: androidx.compose.ui.text.TextMeasurer? = null 294 302 295 303 // Serial queue to serialize session configuration and start/stop calls 296 304 private val sessionQueue = dispatch_queue_create("com.performancecoachlab.captureSessionQueue", null) ··· 604 612 preview, 605 613 width = 480f, 606 614 height = 360f 607 - ).let { FrameSize(it.width.toInt(), it.height.toInt()) } 615 + ).let { FrameSize(it.width.toInt().absoluteValue, it.height.toInt().absoluteValue) } 608 616 }) 609 617 } 610 618 previewObjects.also { objects -> ··· 619 627 bo.first, bo.second 620 628 ).drawResults( 621 629 if (drawSkeleton) previewSkeleton else null, 622 - drawObjects?.invoke(previewObjects) ?: emptyList() 630 + drawObjects?.invoke(previewObjects) ?: emptyList(), 631 + textMeasurer = textMeasurer 623 632 ).also { drawn -> 624 633 cameraViewController?.setRequestDataProvider { 625 634 CameraViewData( ··· 697 706 698 707 val topLeft = mapPoint(box.topLeft) 699 708 val bottomRight = mapPoint(box.bottomRight) 700 - return Rect(topLeft = topLeft, bottomRight = bottomRight) 709 + return Rect(topLeft = topLeft, bottomRight = bottomRight).normalize() 701 710 } 702 711 703 712 override fun captureOutput(
+3
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraView.ios.kt
··· 14 14 import androidx.compose.ui.Modifier 15 15 import androidx.compose.ui.geometry.Rect 16 16 import androidx.compose.ui.layout.ContentScale 17 + import androidx.compose.ui.text.rememberTextMeasurer 17 18 import com.performancecoachlab.posedetection.custom.CustomObjectRespository 18 19 import com.performancecoachlab.posedetection.custom.ObjectModel 19 20 import com.performancecoachlab.posedetection.recording.AnalysisObject ··· 45 46 val frameBitmap by frameListener.frameFlow.collectAsState() 46 47 var lastRecordingState by remember { mutableStateOf(false) } 47 48 var idMap by remember { mutableStateOf<Map<String, String>>(emptyMap()) } 49 + val textMeasurer = rememberTextMeasurer() 48 50 LaunchedEffect(detectMode) { 49 51 cameraEngine.value?.setDetectMode(detectMode) 50 52 } ··· 66 68 drawSkeleton = drawSkeleton, 67 69 drawObjects = drawObjects, 68 70 ) 71 + setTextMeasurer(textMeasurer) 69 72 70 73 if (recordingId != null) { 71 74 (if(lastRecordingState) splitRecording()
+17 -14
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/FrameProcessor.kt
··· 54 54 import platform.Vision.VNRecognizedPoint 55 55 import platform.Vision.VNRequest 56 56 import kotlin.experimental.ExperimentalNativeApi 57 + import kotlin.math.absoluteValue 58 + import kotlin.math.max 59 + import kotlin.math.min 57 60 import kotlin.native.identityHashCode 58 61 59 62 fun bodyPoseHandler(request: VNRequest): List<MutableMap<VNHumanBodyPoseObservationJointName, VNRecognizedPoint>>? { ··· 221 224 val right = (origin.x + size.width) * w 222 225 val bottom = (1.0 - (origin.y + size.height)) * h 223 226 Rect( 224 - left = left.toFloat(), 225 - top = top.toFloat(), 226 - right = right.toFloat(), 227 - bottom = bottom.toFloat() 227 + left = min(left,right).toFloat(), 228 + top = min(top,bottom).toFloat(), 229 + right = max(left,right).toFloat(), 230 + bottom = max(top,bottom).toFloat() 228 231 ) 229 232 } 230 233 val labels = observation.labels.mapNotNull { ··· 238 241 AnalysisObject( 239 242 trackingId = observation.identityHashCode(), 240 243 labels = labels, 241 - boundingBox = boundingBox, 244 + boundingBox = boundingBox.normalize(), 242 245 frameSize = FrameSize( 243 - width = width.toInt(), 244 - height = height.toInt() 246 + width = width.toInt().absoluteValue, 247 + height = height.toInt().absoluteValue 245 248 ) 246 249 ) 247 250 } ··· 400 403 val right = (origin.x + size.width) * w 401 404 val bottom = (1.0 - (origin.y + size.height)) * h 402 405 Rect( 403 - left = left.toFloat(), 404 - top = top.toFloat(), 405 - right = right.toFloat(), 406 - bottom = bottom.toFloat() 406 + left = min(left,right).toFloat(), 407 + top = min(top,bottom).toFloat(), 408 + right = max(left,right).toFloat(), 409 + bottom = max(top,bottom).toFloat() 407 410 ) 408 411 } 409 412 val labels = observation.labels.mapNotNull { ··· 417 420 AnalysisObject( 418 421 trackingId = observation.identityHashCode(), 419 422 labels = labels, 420 - boundingBox = boundingBox, 423 + boundingBox = boundingBox.normalize(), 421 424 frameSize = FrameSize( 422 - width = width.toInt(), 423 - height = height.toInt() 425 + width = width.toInt().absoluteValue, 426 + height = height.toInt().absoluteValue 424 427 ) 425 428 ) 426 429 }
+65
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/LabelTextIos.kt
··· 1 + package com.performancecoachlab.posedetection.camera 2 + 3 + import androidx.compose.ui.geometry.Offset 4 + import androidx.compose.ui.geometry.Size 5 + import androidx.compose.ui.graphics.Color 6 + import androidx.compose.ui.graphics.drawscope.DrawScope 7 + import androidx.compose.ui.graphics.drawscope.clipRect 8 + import androidx.compose.ui.graphics.drawscope.withTransform 9 + import androidx.compose.ui.text.TextLayoutResult 10 + import androidx.compose.ui.text.TextMeasurer 11 + import androidx.compose.ui.text.TextStyle 12 + import androidx.compose.ui.text.drawText 13 + 14 + actual fun DrawScope.drawLabelTextPlatform( 15 + drawableObject: DrawableObject, 16 + textMeasurer: TextMeasurer? 17 + ) { 18 + val label = drawableObject.obj.labels.firstOrNull()?.text.let { if (it.isNullOrBlank()) "Object" else it } 19 + if (label.isEmpty() || textMeasurer == null) return 20 + 21 + val box = drawableObject.obj.boundingBox 22 + val boxLeft = box.left 23 + val boxTop = box.top 24 + val boxWidth = box.width 25 + val boxHeight = box.height 26 + 27 + if (boxWidth <= 0f || boxHeight <= 0f) return 28 + 29 + // Measure once at a base size and re-use that exact layout result for drawing. 30 + // On iOS, measuring and drawing can diverge if we re-measure or rely on fontSize conversions. 31 + val baseStyle = TextStyle( 32 + color = Color.White, 33 + // Use a stable base size; the final size will come from scaling the canvas. 34 + fontSize = 100f.toSp() 35 + ) 36 + 37 + val layout: TextLayoutResult = textMeasurer.measure( 38 + text = label, 39 + style = baseStyle, 40 + maxLines = 1 41 + ) 42 + 43 + val baseWidth = layout.size.width.toFloat() 44 + val baseHeight = layout.size.height.toFloat() 45 + if (baseWidth <= 0f || baseHeight <= 0f) return 46 + 47 + val scale = minOf(boxWidth / baseWidth, boxHeight / baseHeight) 48 + val scaledWidth = baseWidth * scale 49 + val scaledHeight = baseHeight * scale 50 + 51 + val centerX = boxLeft + boxWidth / 2f 52 + val centerY = boxTop + boxHeight / 2f 53 + val x = centerX - scaledWidth / 2f 54 + val y = centerY - scaledHeight / 2f 55 + 56 + withTransform({ 57 + // Keep (x,y) fixed; scale relative to that same top-left. 58 + scale(scaleX = scale, scaleY = scale, pivot = Offset(x, y)) 59 + }) { 60 + drawText( 61 + textLayoutResult = layout, 62 + topLeft = Offset(x, y) 63 + ) 64 + } 65 + }