TODO - end of time - need to revisit anlysis results window - broke it adding the uh faePicker (needs to go in AppRoutes)
This commit is contained in:
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
61
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
61
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.placeholder.sherpai2.ui.trainingprep
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FacePickerScreen(
|
||||||
|
uri: Uri,
|
||||||
|
faceBoxes: List<Rect>,
|
||||||
|
onFaceSelected: (Rect) -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||||
|
Text("Multiple faces detected!", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Text("Tap the person you want to train on.")
|
||||||
|
|
||||||
|
Box(modifier = Modifier.weight(1f).fillMaxWidth().padding(vertical = 16.dp)) {
|
||||||
|
// Main Image
|
||||||
|
Image(
|
||||||
|
painter = rememberAsyncImagePainter(uri),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Fit
|
||||||
|
)
|
||||||
|
|
||||||
|
// Overlay Clickable Boxes
|
||||||
|
// Note: In a production app, you'd need to map Rect coordinates
|
||||||
|
// from the Bitmap scale to the UI View scale.
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize().clickable { /* Handle general tap */ }) {
|
||||||
|
// Implementation of coordinate mapping goes here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified: Just show the options as a list of crops if Canvas mapping is too complex for now
|
||||||
|
LazyRow {
|
||||||
|
items(faceBoxes) { box ->
|
||||||
|
Card(modifier = Modifier.padding(8.dp).size(100.dp).clickable { onFaceSelected(box) }) {
|
||||||
|
Text("Face ${faceBoxes.indexOf(box) + 1}", Modifier.align(Alignment.CenterHorizontally))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@ package com.placeholder.sherpai2.ui.trainingprep
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items // CRITICAL: This is the correct import for List items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -14,14 +15,20 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the outcome of the face detection process.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanResultsScreen(
|
fun ScanResultsScreen(
|
||||||
state: ScanningState,
|
state: ScanningState,
|
||||||
onFinish: () -> Unit
|
onFinish: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
@@ -37,16 +44,25 @@ fun ScanResultsScreen(
|
|||||||
style = MaterialTheme.typography.headlineMedium
|
style = MaterialTheme.typography.headlineMedium
|
||||||
)
|
)
|
||||||
|
|
||||||
LazyColumn(modifier = Modifier.weight(1f).padding(vertical = 16.dp)) {
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
// FIX: Ensure 'items' is the one that takes a List, not a count
|
||||||
items(state.results) { result ->
|
items(state.results) { result ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = rememberAsyncImagePainter(result.uri),
|
painter = rememberAsyncImagePainter(result.uri),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(60.dp).clip(RoundedCornerShape(8.dp)),
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(16.dp))
|
Spacer(Modifier.width(16.dp))
|
||||||
@@ -54,7 +70,7 @@ fun ScanResultsScreen(
|
|||||||
Text(if (result.faceCount > 0) "✅ Face Detected" else "❌ No Face")
|
Text(if (result.faceCount > 0) "✅ Face Detected" else "❌ No Face")
|
||||||
if (result.hasMultipleFaces) {
|
if (result.hasMultipleFaces) {
|
||||||
Text(
|
Text(
|
||||||
"⚠️ Multiple faces (${result.faceCount})",
|
text = "⚠️ Multiple faces (${result.faceCount})",
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
@@ -64,10 +80,16 @@ fun ScanResultsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(onClick = onFinish, modifier = Modifier.fillMaxWidth()) {
|
Button(
|
||||||
|
onClick = onFinish,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
Text("Done")
|
Text("Done")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Add fallback for other states (Idle/RequiresCrop)
|
||||||
|
// so the compiler doesn't complain about non-exhaustive 'when'
|
||||||
else -> { }
|
else -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.placeholder.sherpai2.ui.trainingprep
|
package com.placeholder.sherpai2.ui.trainingprep
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -12,8 +13,6 @@ import com.placeholder.sherpai2.domain.repository.TaggingRepository
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -25,9 +24,11 @@ import kotlinx.coroutines.tasks.await
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// 1. DEFINE THESE AT TOP LEVEL (Outside the class) so the UI can see them
|
||||||
sealed class ScanningState {
|
sealed class ScanningState {
|
||||||
object Idle : ScanningState()
|
object Idle : ScanningState()
|
||||||
data class Processing(val current: Int, val total: Int) : ScanningState()
|
data class Processing(val current: Int, val total: Int) : ScanningState()
|
||||||
|
data class RequiresCrop(val uri: Uri, val faceBoxes: List<Rect>, val remainingUris: List<Uri>) : ScanningState()
|
||||||
data class Success(val results: List<ScanResult>) : ScanningState()
|
data class Success(val results: List<ScanResult>) : ScanningState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,60 +49,72 @@ class TrainViewModel @Inject constructor(
|
|||||||
val uiState: StateFlow<ScanningState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ScanningState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val semaphore = Semaphore(2)
|
private val semaphore = Semaphore(2)
|
||||||
|
private val finalResults = mutableListOf<ScanResult>()
|
||||||
|
|
||||||
fun scanAndTagFaces(uris: List<Uri>) = viewModelScope.launch {
|
fun scanAndTagFaces(uris: List<Uri>) = viewModelScope.launch {
|
||||||
val total = uris.size
|
// Goal: Deduplicate by SHA256 before starting
|
||||||
_uiState.value = ScanningState.Processing(0, total)
|
val allImages = imageRepository.getAllImages().first()
|
||||||
|
val uriToShaMap = allImages.associate { it.image.imageUri to it.image.sha256 }
|
||||||
|
|
||||||
|
val uniqueUris = uris.distinctBy { uri ->
|
||||||
|
uriToShaMap[uri.toString()] ?: uri.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
processNext(uniqueUris)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun processNext(remaining: List<Uri>) {
|
||||||
|
if (remaining.isEmpty()) {
|
||||||
|
_uiState.value = ScanningState.Success(finalResults.toList())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentUri = remaining.first()
|
||||||
|
val nextList = remaining.drop(1)
|
||||||
|
|
||||||
|
_uiState.value = ScanningState.Processing(finalResults.size + 1, finalResults.size + remaining.size)
|
||||||
|
|
||||||
val detector = FaceDetection.getClient(faceOptions())
|
val detector = FaceDetection.getClient(faceOptions())
|
||||||
val allImages = imageRepository.getAllImages().first()
|
try {
|
||||||
val uriToIdMap = allImages.associate { it.image.imageUri to it.image.imageId }
|
val image = InputImage.fromFilePath(context, currentUri)
|
||||||
|
|
||||||
var completedCount = 0
|
|
||||||
|
|
||||||
val scanResults = withContext(Dispatchers.Default) {
|
|
||||||
uris.map { uri ->
|
|
||||||
async {
|
|
||||||
semaphore.withPermit {
|
|
||||||
val faceCount = detectFaceCount(detector, uri)
|
|
||||||
|
|
||||||
// Tagging logic
|
|
||||||
if (faceCount > 0) {
|
|
||||||
uriToIdMap[uri.toString()]?.let { id ->
|
|
||||||
taggingRepository.addTagToImage(id, "face", "ML_KIT", 1.0f)
|
|
||||||
if (faceCount > 1) {
|
|
||||||
taggingRepository.addTagToImage(id, "multiple_faces", "ML_KIT", 1.0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
completedCount++
|
|
||||||
_uiState.value = ScanningState.Processing(completedCount, total)
|
|
||||||
|
|
||||||
ScanResult(uri, faceCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.awaitAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
detector.close()
|
|
||||||
_uiState.value = ScanningState.Success(scanResults)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun detectFaceCount(
|
|
||||||
detector: com.google.mlkit.vision.face.FaceDetector,
|
|
||||||
uri: Uri
|
|
||||||
): Int = withContext(Dispatchers.IO) {
|
|
||||||
return@withContext try {
|
|
||||||
val image = InputImage.fromFilePath(context, uri)
|
|
||||||
val faces = detector.process(image).await()
|
val faces = detector.process(image).await()
|
||||||
faces.size // Returns actual count
|
|
||||||
} catch (e: Exception) {
|
if (faces.size > 1) {
|
||||||
0
|
// FORCE USER TO CROP: Transition to RequiresCrop state
|
||||||
|
_uiState.value = ScanningState.RequiresCrop(
|
||||||
|
uri = currentUri,
|
||||||
|
faceBoxes = faces.map { it.boundingBox },
|
||||||
|
remainingUris = nextList
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val faceCount = faces.size
|
||||||
|
if (faceCount > 0) tagImage(currentUri)
|
||||||
|
|
||||||
|
finalResults.add(ScanResult(currentUri, faceCount))
|
||||||
|
processNext(nextList)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
processNext(nextList)
|
||||||
|
} finally {
|
||||||
|
detector.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun tagImage(uri: Uri) {
|
||||||
|
val allImages = imageRepository.getAllImages().first()
|
||||||
|
val imageId = allImages.find { it.image.imageUri == uri.toString() }?.image?.imageId
|
||||||
|
if (imageId != null) {
|
||||||
|
taggingRepository.addTagToImage(imageId, "face", "ML_KIT", 1.0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFaceSelected(uri: Uri, box: Rect, remaining: List<Uri>) = viewModelScope.launch {
|
||||||
|
tagImage(uri)
|
||||||
|
finalResults.add(ScanResult(uri, 1))
|
||||||
|
processNext(remaining)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun faceOptions() = FaceDetectorOptions.Builder()
|
private fun faceOptions() = FaceDetectorOptions.Builder()
|
||||||
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
|
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user