Pre UI Sweep
This commit is contained in:
7
.idea/deviceManager.xml
generated
7
.idea/deviceManager.xml
generated
@@ -5,9 +5,14 @@
|
|||||||
<list>
|
<list>
|
||||||
<ColumnSorterState>
|
<ColumnSorterState>
|
||||||
<option name="column" value="Name" />
|
<option name="column" value="Name" />
|
||||||
<option name="order" value="ASCENDING" />
|
<option name="order" value="DESCENDING" />
|
||||||
</ColumnSorterState>
|
</ColumnSorterState>
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="groupByAttributes">
|
||||||
|
<list>
|
||||||
|
<option value="Type" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -18,8 +18,7 @@ import androidx.compose.material.icons.filled.*
|
|||||||
import com.placeholder.sherpai2.ui.navigation.AppRoutes
|
import com.placeholder.sherpai2.ui.navigation.AppRoutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beautiful app drawer with sections, gradient header, and polish
|
* SLIMMED DOWN AppDrawer - 280dp width, inline logo, cleaner sections
|
||||||
* UPDATED: Tour → Explore
|
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -28,12 +27,12 @@ fun AppDrawerContent(
|
|||||||
onDestinationClicked: (String) -> Unit
|
onDestinationClicked: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
ModalDrawerSheet(
|
ModalDrawerSheet(
|
||||||
modifier = Modifier.width(300.dp),
|
modifier = Modifier.width(280.dp), // SLIMMER (was 300dp)
|
||||||
drawerContainerColor = MaterialTheme.colorScheme.surface
|
drawerContainerColor = MaterialTheme.colorScheme.surface
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|
||||||
// ===== BEAUTIFUL GRADIENT HEADER =====
|
// ===== COMPACT HEADER - Icon + Text Inline =====
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -45,15 +44,16 @@ fun AppDrawerContent(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.padding(24.dp)
|
.padding(20.dp) // Reduced padding
|
||||||
) {
|
) {
|
||||||
Column(
|
Row(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// App icon/logo area
|
// App icon - smaller
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.size(56.dp),
|
modifier = Modifier.size(48.dp), // Smaller (was 56dp)
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
shadowElevation = 4.dp
|
shadowElevation = 4.dp
|
||||||
) {
|
) {
|
||||||
@@ -61,44 +61,47 @@ fun AppDrawerContent(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Face,
|
Icons.Default.Face,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
tint = MaterialTheme.colorScheme.onPrimary
|
tint = MaterialTheme.colorScheme.onPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Text next to icon
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
Text(
|
Text(
|
||||||
"SherpAI",
|
"SherpAI",
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.titleLarge, // Smaller (was headlineMedium)
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"Face Recognition System",
|
"Face Recognition System",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodySmall, // Smaller
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(4.dp)) // Reduced spacing
|
||||||
|
|
||||||
// ===== NAVIGATION SECTIONS =====
|
// ===== NAVIGATION SECTIONS =====
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 12.dp),
|
.padding(horizontal = 8.dp), // Reduced padding
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp) // Tighter spacing
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Photos Section
|
// Photos Section
|
||||||
DrawerSection(title = "Photos")
|
DrawerSection(title = "Photos")
|
||||||
|
|
||||||
val photoItems = listOf(
|
val photoItems = listOf(
|
||||||
DrawerItem(AppRoutes.SEARCH, "Search", Icons.Default.Search, "Find photos by tag or person"),
|
DrawerItem(AppRoutes.SEARCH, "Search", Icons.Default.Search),
|
||||||
DrawerItem(AppRoutes.EXPLORE, "Explore", Icons.Default.Explore, "Browse smart albums")
|
DrawerItem(AppRoutes.EXPLORE, "Explore", Icons.Default.Explore)
|
||||||
)
|
)
|
||||||
|
|
||||||
photoItems.forEach { item ->
|
photoItems.forEach { item ->
|
||||||
@@ -109,15 +112,15 @@ fun AppDrawerContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
// Face Recognition Section
|
// Face Recognition Section
|
||||||
DrawerSection(title = "Face Recognition")
|
DrawerSection(title = "Face Recognition")
|
||||||
|
|
||||||
val faceItems = listOf(
|
val faceItems = listOf(
|
||||||
DrawerItem(AppRoutes.INVENTORY, "People", Icons.Default.Face, "Existing models"),
|
DrawerItem(AppRoutes.INVENTORY, "People", Icons.Default.Face),
|
||||||
DrawerItem(AppRoutes.TRAIN, "Create Person", Icons.Default.ModelTraining, "New person model"),
|
DrawerItem(AppRoutes.TRAIN, "Train New", Icons.Default.ModelTraining),
|
||||||
DrawerItem(AppRoutes.MODELS, "Models", Icons.Default.SmartToy, "AI model management")
|
DrawerItem(AppRoutes.MODELS, "Models", Icons.Default.SmartToy)
|
||||||
)
|
)
|
||||||
|
|
||||||
faceItems.forEach { item ->
|
faceItems.forEach { item ->
|
||||||
@@ -128,14 +131,14 @@ fun AppDrawerContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
// Organization Section
|
// Organization Section
|
||||||
DrawerSection(title = "Organization")
|
DrawerSection(title = "Organization")
|
||||||
|
|
||||||
val orgItems = listOf(
|
val orgItems = listOf(
|
||||||
DrawerItem(AppRoutes.TAGS, "Tags", Icons.AutoMirrored.Filled.Label, "Manage Tags"),
|
DrawerItem(AppRoutes.TAGS, "Tags", Icons.AutoMirrored.Filled.Label),
|
||||||
DrawerItem(AppRoutes.UTILITIES, "Util.", Icons.Default.NewReleases, "Manage Collection")
|
DrawerItem(AppRoutes.UTILITIES, "Utilities", Icons.Default.Build)
|
||||||
)
|
)
|
||||||
|
|
||||||
orgItems.forEach { item ->
|
orgItems.forEach { item ->
|
||||||
@@ -150,7 +153,7 @@ fun AppDrawerContent(
|
|||||||
|
|
||||||
// Settings at bottom
|
// Settings at bottom
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(vertical = 8.dp),
|
modifier = Modifier.padding(vertical = 6.dp),
|
||||||
color = MaterialTheme.colorScheme.outlineVariant
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -158,35 +161,34 @@ fun AppDrawerContent(
|
|||||||
item = DrawerItem(
|
item = DrawerItem(
|
||||||
AppRoutes.SETTINGS,
|
AppRoutes.SETTINGS,
|
||||||
"Settings",
|
"Settings",
|
||||||
Icons.Default.Settings,
|
Icons.Default.Settings
|
||||||
"App preferences"
|
|
||||||
),
|
),
|
||||||
selected = AppRoutes.SETTINGS == currentRoute,
|
selected = AppRoutes.SETTINGS == currentRoute,
|
||||||
onClick = { onDestinationClicked(AppRoutes.SETTINGS) }
|
onClick = { onDestinationClicked(AppRoutes.SETTINGS) }
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Section header in drawer
|
* Section header - more compact
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun DrawerSection(title: String) {
|
private fun DrawerSection(title: String) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelSmall, // Smaller
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) // Reduced padding
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Individual navigation item with icon, label, and subtitle
|
* Navigation item - cleaner, no subtitle
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun DrawerNavigationItem(
|
private fun DrawerNavigationItem(
|
||||||
@@ -196,33 +198,24 @@ private fun DrawerNavigationItem(
|
|||||||
) {
|
) {
|
||||||
NavigationDrawerItem(
|
NavigationDrawerItem(
|
||||||
label = {
|
label = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
|
||||||
Text(
|
Text(
|
||||||
text = item.label,
|
text = item.label,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyMedium, // Slightly smaller
|
||||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
|
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
|
||||||
)
|
)
|
||||||
item.subtitle?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
item.icon,
|
item.icon,
|
||||||
contentDescription = item.label,
|
contentDescription = item.label,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(22.dp) // Slightly smaller
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(NavigationDrawerItemDefaults.ItemPadding)
|
.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
.clip(RoundedCornerShape(10.dp)), // Slightly smaller radius
|
||||||
colors = NavigationDrawerItemDefaults.colors(
|
colors = NavigationDrawerItemDefaults.colors(
|
||||||
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
selectedIconColor = MaterialTheme.colorScheme.primary,
|
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||||
@@ -233,11 +226,10 @@ private fun DrawerNavigationItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data class for drawer items
|
* Simplified drawer item (no subtitle)
|
||||||
*/
|
*/
|
||||||
private data class DrawerItem(
|
private data class DrawerItem(
|
||||||
val route: String,
|
val route: String,
|
||||||
val label: String,
|
val label: String,
|
||||||
val icon: androidx.compose.ui.graphics.vector.ImageVector,
|
val icon: androidx.compose.ui.graphics.vector.ImageVector
|
||||||
val subtitle: String? = null
|
|
||||||
)
|
)
|
||||||
@@ -5,65 +5,99 @@ import android.graphics.BitmapFactory
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import com.google.mlkit.vision.common.InputImage
|
||||||
|
import com.google.mlkit.vision.face.FaceDetection
|
||||||
|
import com.google.mlkit.vision.face.FaceDetectorOptions
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.tasks.await
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog for selecting a face from multiple detected faces
|
* Dialog for selecting a face from multiple detected faces
|
||||||
|
*
|
||||||
|
* CRITICAL: Re-detects faces on full resolution bitmap to ensure accurate cropping.
|
||||||
|
* Face bounds from FaceDetectionHelper are from downsampled images and won't match
|
||||||
|
* the full resolution bitmap loaded here.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun FacePickerDialog(
|
fun FacePickerDialog(
|
||||||
result: FaceDetectionHelper.FaceDetectionResult,
|
result: FaceDetectionHelper.FaceDetectionResult,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onFaceSelected: (Int, Bitmap) -> Unit // faceIndex, croppedFaceBitmap
|
onFaceSelected: (Int, Bitmap) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var selectedFaceIndex by remember { mutableStateOf<Int?>(null) }
|
var selectedFaceIndex by remember { mutableStateOf(0) }
|
||||||
var croppedFaces by remember { mutableStateOf<List<Bitmap>>(emptyList()) }
|
var croppedFaces by remember { mutableStateOf<List<Bitmap>>(emptyList()) }
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
// Load and crop all faces
|
// Load and crop all faces - RE-DETECT to get accurate bounds
|
||||||
LaunchedEffect(result) {
|
LaunchedEffect(result) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
errorMessage = null
|
||||||
|
|
||||||
|
try {
|
||||||
croppedFaces = withContext(Dispatchers.IO) {
|
croppedFaces = withContext(Dispatchers.IO) {
|
||||||
val bitmap = loadBitmapFromUri(context, result.uri)
|
// Load the FULL resolution bitmap (no downsampling)
|
||||||
bitmap?.let { bmp ->
|
val fullBitmap = loadFullResolutionBitmap(context, result.uri)
|
||||||
result.faceBounds.map { bounds ->
|
|
||||||
cropFaceFromBitmap(bmp, bounds)
|
if (fullBitmap == null) {
|
||||||
|
errorMessage = "Failed to load image"
|
||||||
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
} ?: emptyList()
|
|
||||||
|
// Re-detect faces on the full resolution bitmap to get accurate bounds
|
||||||
|
val accurateFaceBounds = detectFacesOnBitmap(fullBitmap)
|
||||||
|
|
||||||
|
if (accurateFaceBounds.isEmpty()) {
|
||||||
|
// Fallback: try to use the original bounds with scaling
|
||||||
|
val scaledBounds = result.faceBounds.map { originalBounds ->
|
||||||
|
cropFaceFromBitmap(fullBitmap, originalBounds)
|
||||||
}
|
}
|
||||||
|
fullBitmap.recycle()
|
||||||
|
return@withContext scaledBounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crop faces using accurate bounds
|
||||||
|
val croppedList = accurateFaceBounds.map { bounds ->
|
||||||
|
cropFaceFromBitmap(fullBitmap, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL: Recycle AFTER all cropping is done
|
||||||
|
fullBitmap.recycle()
|
||||||
|
|
||||||
|
croppedList
|
||||||
|
}
|
||||||
|
|
||||||
|
if (croppedFaces.isEmpty() && errorMessage == null) {
|
||||||
|
errorMessage = "No faces found in full resolution image"
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = "Error processing faces: ${e.message}"
|
||||||
|
} finally {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
// Auto-select the first (largest) face
|
|
||||||
if (croppedFaces.isNotEmpty()) {
|
|
||||||
selectedFaceIndex = 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,15 +107,18 @@ fun FacePickerDialog(
|
|||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.95f)
|
.fillMaxWidth(0.94f)
|
||||||
.fillMaxHeight(0.9f),
|
.wrapContentHeight(),
|
||||||
shape = RoundedCornerShape(16.dp)
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
.padding(20.dp),
|
.padding(24.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||||
) {
|
) {
|
||||||
// Header
|
// Header
|
||||||
Row(
|
Row(
|
||||||
@@ -92,7 +129,7 @@ fun FacePickerDialog(
|
|||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = "Pick a Face",
|
text = "Pick a Face",
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -103,14 +140,19 @@ fun FacePickerDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = onDismiss) {
|
IconButton(onClick = onDismiss) {
|
||||||
Icon(Icons.Default.Close, "Close")
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = "Close",
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instruction
|
// Instruction
|
||||||
Text(
|
Text(
|
||||||
text = "Tap a face below to select it for training:",
|
text = "Tap a face below to select it for training:",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -118,43 +160,75 @@ fun FacePickerDialog(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.height(200.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
|
Text(
|
||||||
|
"Processing faces...",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (errorMessage != null) {
|
||||||
|
// Error state
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = errorMessage ?: "Unknown error",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Original image with face boxes overlay
|
// Original image preview
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.height(200.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
) {
|
) {
|
||||||
Box(
|
AsyncImage(
|
||||||
|
model = result.uri,
|
||||||
|
contentDescription = "Original image",
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentScale = ContentScale.Fit
|
||||||
) {
|
|
||||||
FaceOverlayImage(
|
|
||||||
imageUri = result.uri,
|
|
||||||
faceBounds = result.faceBounds,
|
|
||||||
selectedFaceIndex = selectedFaceIndex,
|
|
||||||
onFaceClick = { index ->
|
|
||||||
selectedFaceIndex = index
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Face previews grid
|
// Face previews section
|
||||||
Text(
|
Text(
|
||||||
text = "Preview (tap to select):",
|
text = "Preview (tap to select):",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Face preview cards
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
@@ -162,13 +236,41 @@ fun FacePickerDialog(
|
|||||||
croppedFaces.forEachIndexed { index, faceBitmap ->
|
croppedFaces.forEachIndexed { index, faceBitmap ->
|
||||||
FacePreviewCard(
|
FacePreviewCard(
|
||||||
faceBitmap = faceBitmap,
|
faceBitmap = faceBitmap,
|
||||||
index = index,
|
index = index + 1,
|
||||||
isSelected = selectedFaceIndex == index,
|
isSelected = selectedFaceIndex == index,
|
||||||
onClick = { selectedFaceIndex = index },
|
onClick = { selectedFaceIndex = index },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper text
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"The selected face will be used for training",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
@@ -178,25 +280,33 @@ fun FacePickerDialog(
|
|||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onDismiss,
|
onClick = onDismiss,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(52.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp)
|
||||||
) {
|
) {
|
||||||
Text("Cancel")
|
Text("Cancel", style = MaterialTheme.typography.titleMedium)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedFaceIndex?.let { index ->
|
if (selectedFaceIndex < croppedFaces.size) {
|
||||||
if (index < croppedFaces.size) {
|
onFaceSelected(selectedFaceIndex, croppedFaces[selectedFaceIndex])
|
||||||
onFaceSelected(index, croppedFaces[index])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
enabled = selectedFaceIndex != null && !isLoading
|
.weight(1f)
|
||||||
|
.height(52.dp),
|
||||||
|
enabled = !isLoading && croppedFaces.isNotEmpty() && errorMessage == null,
|
||||||
|
shape = RoundedCornerShape(14.dp)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.CheckCircle, contentDescription = null)
|
Icon(
|
||||||
|
Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text("Select")
|
Text("Use This Face", style = MaterialTheme.typography.titleMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,119 +314,6 @@ fun FacePickerDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Image with interactive face boxes overlay
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun FaceOverlayImage(
|
|
||||||
imageUri: Uri,
|
|
||||||
faceBounds: List<Rect>,
|
|
||||||
selectedFaceIndex: Int?,
|
|
||||||
onFaceClick: (Int) -> Unit
|
|
||||||
) {
|
|
||||||
var imageSize by remember { mutableStateOf(Size.Zero) }
|
|
||||||
var imageBounds by remember { mutableStateOf(Rect()) }
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
// Original image
|
|
||||||
AsyncImage(
|
|
||||||
model = imageUri,
|
|
||||||
contentDescription = "Original image",
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(8.dp),
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
onSuccess = { state ->
|
|
||||||
val drawable = state.result.drawable
|
|
||||||
imageBounds = Rect(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Face boxes overlay
|
|
||||||
Canvas(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
if (imageBounds.width() > 0 && imageBounds.height() > 0) {
|
|
||||||
// Calculate scale to fit image in canvas
|
|
||||||
val scaleX = size.width / imageBounds.width()
|
|
||||||
val scaleY = size.height / imageBounds.height()
|
|
||||||
val scale = minOf(scaleX, scaleY)
|
|
||||||
|
|
||||||
// Calculate offset to center image
|
|
||||||
val scaledWidth = imageBounds.width() * scale
|
|
||||||
val scaledHeight = imageBounds.height() * scale
|
|
||||||
val offsetX = (size.width - scaledWidth) / 2
|
|
||||||
val offsetY = (size.height - scaledHeight) / 2
|
|
||||||
|
|
||||||
faceBounds.forEachIndexed { index, bounds ->
|
|
||||||
val isSelected = selectedFaceIndex == index
|
|
||||||
|
|
||||||
// Scale and position the face box
|
|
||||||
val left = bounds.left * scale + offsetX
|
|
||||||
val top = bounds.top * scale + offsetY
|
|
||||||
val width = bounds.width() * scale
|
|
||||||
val height = bounds.height() * scale
|
|
||||||
|
|
||||||
// Draw box
|
|
||||||
drawRect(
|
|
||||||
color = if (isSelected) Color(0xFF4CAF50) else Color(0xFF2196F3),
|
|
||||||
topLeft = Offset(left, top),
|
|
||||||
size = Size(width, height),
|
|
||||||
style = Stroke(width = if (isSelected) 6f else 4f)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw semi-transparent fill for selected
|
|
||||||
if (isSelected) {
|
|
||||||
drawRect(
|
|
||||||
color = Color(0xFF4CAF50).copy(alpha = 0.2f),
|
|
||||||
topLeft = Offset(left, top),
|
|
||||||
size = Size(width, height)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw face number label
|
|
||||||
drawCircle(
|
|
||||||
color = if (isSelected) Color(0xFF4CAF50) else Color(0xFF2196F3),
|
|
||||||
radius = 20f * scale,
|
|
||||||
center = Offset(left + 20f * scale, top + 20f * scale)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clickable areas for each face
|
|
||||||
faceBounds.forEachIndexed { index, bounds ->
|
|
||||||
if (imageBounds.width() > 0 && imageBounds.height() > 0) {
|
|
||||||
val scaleX = imageSize.width / imageBounds.width()
|
|
||||||
val scaleY = imageSize.height / imageBounds.height()
|
|
||||||
val scale = minOf(scaleX, scaleY)
|
|
||||||
|
|
||||||
val scaledWidth = imageBounds.width() * scale
|
|
||||||
val scaledHeight = imageBounds.height() * scale
|
|
||||||
val offsetX = (imageSize.width - scaledWidth) / 2
|
|
||||||
val offsetY = (imageSize.height - scaledHeight) / 2
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clickable { onFaceClick(index) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update image size
|
|
||||||
BoxWithConstraints {
|
|
||||||
LaunchedEffect(constraints) {
|
|
||||||
imageSize = Size(constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Individual face preview card
|
* Individual face preview card
|
||||||
*/
|
*/
|
||||||
@@ -330,69 +327,88 @@ private fun FacePreviewCard(
|
|||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(1f)
|
.aspectRatio(0.75f)
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (isSelected)
|
containerColor = if (isSelected)
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
else
|
else
|
||||||
MaterialTheme.colorScheme.surface
|
MaterialTheme.colorScheme.surfaceVariant
|
||||||
),
|
),
|
||||||
border = if (isSelected)
|
border = if (isSelected)
|
||||||
BorderStroke(3.dp, MaterialTheme.colorScheme.primary)
|
BorderStroke(3.dp, MaterialTheme.colorScheme.primary)
|
||||||
else
|
else
|
||||||
BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
|
BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(
|
||||||
|
defaultElevation = if (isSelected) 8.dp else 2.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// Face image
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
androidx.compose.foundation.Image(
|
Image(
|
||||||
bitmap = faceBitmap.asImageBitmap(),
|
bitmap = faceBitmap.asImageBitmap(),
|
||||||
contentDescription = "Face ${index + 1}",
|
contentDescription = "Face $index",
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
|
||||||
// Selected checkmark (only show when selected)
|
// Selected overlay with checkmark
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.align(Alignment.Center),
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shadowElevation = 4.dp
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CheckCircle,
|
Icons.Default.CheckCircle,
|
||||||
contentDescription = "Selected",
|
contentDescription = "Selected",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
.size(32.dp),
|
.size(40.dp),
|
||||||
tint = MaterialTheme.colorScheme.onPrimary
|
tint = MaterialTheme.colorScheme.onPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Face number badge (always in top-right, small)
|
// Face number label
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.padding(4.dp),
|
|
||||||
shape = CircleShape,
|
|
||||||
color = if (isSelected)
|
color = if (isSelected)
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
else
|
else
|
||||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.9f),
|
MaterialTheme.colorScheme.surface,
|
||||||
shadowElevation = 2.dp
|
shape = RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${index + 1}",
|
text = "Face $index",
|
||||||
modifier = Modifier.padding(6.dp),
|
modifier = Modifier.padding(vertical = 12.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
color = if (isSelected)
|
color = if (isSelected)
|
||||||
MaterialTheme.colorScheme.onPrimary
|
MaterialTheme.colorScheme.onPrimary
|
||||||
else
|
else
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,14 +416,14 @@ private fun FacePreviewCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to load bitmap from URI
|
* Load full resolution bitmap WITHOUT downsampling
|
||||||
*/
|
*/
|
||||||
private suspend fun loadBitmapFromUri(
|
private suspend fun loadFullResolutionBitmap(
|
||||||
context: android.content.Context,
|
context: android.content.Context,
|
||||||
uri: Uri
|
uri: Uri
|
||||||
): Bitmap? = withContext(Dispatchers.IO) {
|
): Bitmap? = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||||
BitmapFactory.decodeStream(inputStream)?.also {
|
BitmapFactory.decodeStream(inputStream)?.also {
|
||||||
inputStream?.close()
|
inputStream?.close()
|
||||||
}
|
}
|
||||||
@@ -417,7 +433,34 @@ private suspend fun loadBitmapFromUri(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to crop face from bitmap
|
* Re-detect faces on full resolution bitmap to get accurate bounds
|
||||||
|
*/
|
||||||
|
private suspend fun detectFacesOnBitmap(bitmap: Bitmap): List<Rect> = withContext(Dispatchers.Default) {
|
||||||
|
try {
|
||||||
|
val faceDetectorOptions = FaceDetectorOptions.Builder()
|
||||||
|
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
|
||||||
|
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
|
||||||
|
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE)
|
||||||
|
.setMinFaceSize(0.15f)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val detector = FaceDetection.getClient(faceDetectorOptions)
|
||||||
|
val inputImage = InputImage.fromBitmap(bitmap, 0)
|
||||||
|
|
||||||
|
val faces = detector.process(inputImage).await()
|
||||||
|
|
||||||
|
// Sort by face size (largest first)
|
||||||
|
faces.sortedByDescending { face ->
|
||||||
|
face.boundingBox.width() * face.boundingBox.height()
|
||||||
|
}.map { it.boundingBox }
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to crop face from bitmap with padding
|
||||||
*/
|
*/
|
||||||
private fun cropFaceFromBitmap(bitmap: Bitmap, faceBounds: Rect): Bitmap {
|
private fun cropFaceFromBitmap(bitmap: Bitmap, faceBounds: Rect): Bitmap {
|
||||||
// Add 20% padding around the face
|
// Add 20% padding around the face
|
||||||
|
|||||||
Reference in New Issue
Block a user