Distance and collect images auto
This commit is contained in:
parent
3e5a8772d7
commit
6de6ef8bf8
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -1,14 +1,17 @@
|
|||
package com.example.animalrating
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentValues
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.graphics.Matrix
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.widget.Button
|
||||
import android.util.Size
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
|
@ -23,6 +26,7 @@ import androidx.core.content.ContextCompat
|
|||
import com.example.animalrating.ml.CowAnalyzer
|
||||
import com.example.animalrating.ui.SilhouetteOverlay
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
|
@ -41,6 +45,9 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
private var orientation: String? = null
|
||||
private var currentMask: Bitmap? = null
|
||||
private var savedMaskBitmap: Bitmap? = null
|
||||
private var isPhotoTaken = false
|
||||
private var matchThreshold = 75
|
||||
private var algorithm = HomeActivity.ALGORITHM_HAMMING
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -49,6 +56,11 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
cowName = intent.getStringExtra("COW_NAME")
|
||||
orientation = intent.getStringExtra("ORIENTATION")
|
||||
|
||||
// Load settings
|
||||
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
|
||||
matchThreshold = prefs.getInt("THRESHOLD", 75)
|
||||
algorithm = prefs.getString("ALGORITHM", HomeActivity.ALGORITHM_HAMMING) ?: HomeActivity.ALGORITHM_HAMMING
|
||||
|
||||
// Set orientation based on selected view
|
||||
if (orientation == "front" || orientation == "back") {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
|
|
@ -61,33 +73,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
segmentationOverlay = findViewById(R.id.segmentationOverlay)
|
||||
savedMaskOverlay = findViewById(R.id.savedMaskOverlay)
|
||||
|
||||
frameProcessor = FrameProcessor { maskBitmap ->
|
||||
runOnUiThread {
|
||||
if (maskBitmap != null && (orientation == "front" || orientation == "back")) {
|
||||
try {
|
||||
val matrix = Matrix()
|
||||
matrix.postRotate(90f)
|
||||
val rotatedBitmap = Bitmap.createBitmap(
|
||||
maskBitmap, 0, 0, maskBitmap.width, maskBitmap.height, matrix, true
|
||||
)
|
||||
currentMask = rotatedBitmap
|
||||
segmentationOverlay.setImageBitmap(rotatedBitmap)
|
||||
} catch (e: Exception) {
|
||||
Log.e("CameraProcessor", "Error rotating mask", e)
|
||||
currentMask = maskBitmap
|
||||
segmentationOverlay.setImageBitmap(maskBitmap)
|
||||
}
|
||||
} else {
|
||||
currentMask = maskBitmap
|
||||
segmentationOverlay.setImageBitmap(maskBitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val btnSave = findViewById<Button>(R.id.btnSave)
|
||||
btnSave.setOnClickListener {
|
||||
saveBitmask()
|
||||
}
|
||||
frameProcessor = FrameProcessor()
|
||||
|
||||
val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0)
|
||||
overlay.setSilhouette(silhouetteId)
|
||||
|
|
@ -98,6 +84,9 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
}
|
||||
|
||||
private fun takePhoto() {
|
||||
if (isPhotoTaken) return
|
||||
isPhotoTaken = true
|
||||
|
||||
val imageCapture = imageCapture ?: return
|
||||
|
||||
val name = cowName ?: "unknown"
|
||||
|
|
@ -121,6 +110,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
override fun onError(exc: ImageCaptureException) {
|
||||
Log.e("MainActivity", "Photo capture failed: ${exc.message}", exc)
|
||||
Toast.makeText(baseContext, "Photo capture failed", Toast.LENGTH_SHORT).show()
|
||||
isPhotoTaken = false
|
||||
}
|
||||
|
||||
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
||||
|
|
@ -139,18 +129,57 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
}
|
||||
}
|
||||
|
||||
// Save to public gallery
|
||||
saveToGallery(file)
|
||||
|
||||
val msg = "Saved as $filename"
|
||||
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error rotating image", e)
|
||||
Log.e("MainActivity", "Error saving image", e)
|
||||
Toast.makeText(baseContext, "Error saving image", Toast.LENGTH_SHORT).show()
|
||||
isPhotoTaken = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveToGallery(file: File) {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.DISPLAY_NAME, file.name)
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/AnimalRating")
|
||||
put(MediaStore.Images.Media.IS_PENDING, 1)
|
||||
}
|
||||
}
|
||||
|
||||
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
} else {
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
|
||||
try {
|
||||
val uri = contentResolver.insert(collection, values)
|
||||
uri?.let {
|
||||
contentResolver.openOutputStream(it)?.use { out ->
|
||||
FileInputStream(file).use { input ->
|
||||
input.copyTo(out)
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
values.clear()
|
||||
values.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||
contentResolver.update(it, values, null, null)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("CameraProcessor", "Error saving to gallery", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSavedMask() {
|
||||
val side = orientation ?: "unknown"
|
||||
|
|
@ -160,12 +189,19 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
if (file.exists()) {
|
||||
try {
|
||||
val savedBitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
savedMaskBitmap = savedBitmap // Store reference for comparison
|
||||
|
||||
// Apply green color filter or process pixels
|
||||
// Apply green color filter for visualization (on original size)
|
||||
val greenMask = applyGreenColor(savedBitmap)
|
||||
savedMaskOverlay.setImageBitmap(greenMask)
|
||||
savedMaskOverlay.alpha = 0.5f
|
||||
|
||||
// Scale to 640x480 (or flipped for portrait) for comparison
|
||||
val isPortrait = (side == "front" || side == "back")
|
||||
val width = if (isPortrait) 480 else 640
|
||||
val height = if (isPortrait) 640 else 480
|
||||
|
||||
savedMaskBitmap = Bitmap.createScaledBitmap(savedBitmap, width, height, true)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("CameraProcessor", "Error loading saved mask", e)
|
||||
}
|
||||
|
|
@ -181,69 +217,13 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
for (i in pixels.indices) {
|
||||
val alpha = (pixels[i] shr 24) and 0xff
|
||||
if (alpha > 10) {
|
||||
// Set to Green with original alpha or fixed alpha
|
||||
// Set to Green with original alpha
|
||||
pixels[i] = Color.argb(alpha, 0, 255, 0)
|
||||
}
|
||||
}
|
||||
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
|
||||
private fun saveBitmask() {
|
||||
val mask = currentMask
|
||||
if (mask == null) {
|
||||
Toast.makeText(this, "No mask available to save", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val side = orientation ?: "unknown"
|
||||
|
||||
val filename = "${side}_mask.png"
|
||||
val file = File(filesDir, filename)
|
||||
|
||||
try {
|
||||
FileOutputStream(file).use { out ->
|
||||
mask.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
}
|
||||
Toast.makeText(this, "Bitmask saved as $filename", Toast.LENGTH_SHORT).show()
|
||||
|
||||
// Reload to show the newly saved mask immediately
|
||||
loadSavedMask()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("CameraProcessor", "Error saving bitmask", e)
|
||||
Toast.makeText(this, "Error saving bitmask", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// Function to calculate Hamming distance between two bitmasks
|
||||
fun calculateHammingDistance(mask1: Bitmap, mask2: Bitmap, threshold: Int): Boolean {
|
||||
if (mask1.width != mask2.width || mask1.height != mask2.height) {
|
||||
Log.w("CameraProcessor", "Masks dimensions mismatch: ${mask1.width}x${mask1.height} vs ${mask2.width}x${mask2.height}")
|
||||
return false
|
||||
}
|
||||
|
||||
val width = mask1.width
|
||||
val height = mask1.height
|
||||
val pixels1 = IntArray(width * height)
|
||||
val pixels2 = IntArray(width * height)
|
||||
|
||||
mask1.getPixels(pixels1, 0, width, 0, 0, width, height)
|
||||
mask2.getPixels(pixels2, 0, width, 0, 0, width, height)
|
||||
|
||||
var distance = 0
|
||||
for (i in pixels1.indices) {
|
||||
val isSet1 = (pixels1[i] ushr 24) > 0
|
||||
val isSet2 = (pixels2[i] ushr 24) > 0
|
||||
|
||||
if (isSet1 != isSet2) {
|
||||
distance++
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("CameraProcessor", "Hamming distance: $distance")
|
||||
return distance <= threshold
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
if (granted) startCamera()
|
||||
|
|
@ -256,13 +236,14 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
val cameraProvider = providerFuture.get()
|
||||
|
||||
val preview = androidx.camera.core.Preview.Builder().build()
|
||||
preview.setSurfaceProvider(previewView.surfaceProvider)
|
||||
preview.surfaceProvider = previewView.surfaceProvider
|
||||
|
||||
imageCapture = ImageCapture.Builder()
|
||||
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
||||
.build()
|
||||
|
||||
val analyzer = ImageAnalysis.Builder()
|
||||
.setTargetResolution(Size(640, 480))
|
||||
.setBackpressureStrategy(
|
||||
ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
|
||||
)
|
||||
|
|
@ -284,18 +265,27 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
|
|||
}
|
||||
|
||||
override fun onFrame(imageProxy: ImageProxy) {
|
||||
val current = frameProcessor.processFrame(imageProxy)
|
||||
val savedMask = savedMaskBitmap
|
||||
val threshold = 80000
|
||||
Log.d("MatchingMasks", "Comparing masks: ${current?.width} ${savedMask?.width}")
|
||||
if (savedMask != null && current != null) {
|
||||
// Perform comparison
|
||||
Log.d("MatchingMasks", "Comparing masks")
|
||||
if (calculateHammingDistance(savedMask, current, threshold)) {
|
||||
takePhoto()
|
||||
} else {
|
||||
Toast.makeText(this, "Masks do not match. Please align better.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (isPhotoTaken) {
|
||||
imageProxy.close()
|
||||
return
|
||||
}
|
||||
|
||||
val isPortrait = (orientation == "front" || orientation == "back")
|
||||
|
||||
frameProcessor.processFrame(imageProxy, savedMaskBitmap, isPortrait, matchThreshold, algorithm)
|
||||
.addOnSuccessListener { result ->
|
||||
runOnUiThread {
|
||||
if (result.mask != null) {
|
||||
currentMask = result.mask
|
||||
segmentationOverlay.setImageBitmap(result.mask)
|
||||
}
|
||||
}
|
||||
if (result.isMatch) {
|
||||
takePhoto()
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e("CameraProcessor", "Frame processing error", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,16 +2,25 @@ package com.example.animalrating
|
|||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.Matrix
|
||||
import android.util.Log
|
||||
import androidx.camera.core.ExperimentalGetImage
|
||||
import androidx.camera.core.ImageProxy
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.android.gms.tasks.TaskCompletionSource
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
|
||||
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class FrameProcessor(private val onMaskReady: (Bitmap?) -> Unit) {
|
||||
data class SegmentationResult(
|
||||
val mask: Bitmap?,
|
||||
val isMatch: Boolean
|
||||
)
|
||||
|
||||
class FrameProcessor {
|
||||
|
||||
private val isProcessing = AtomicBoolean(false)
|
||||
private val processingExecutor = Executors.newSingleThreadExecutor()
|
||||
|
|
@ -26,25 +35,34 @@ class FrameProcessor(private val onMaskReady: (Bitmap?) -> Unit) {
|
|||
private val segmenter = SubjectSegmentation.getClient(options)
|
||||
|
||||
@ExperimentalGetImage
|
||||
fun processFrame(imageProxy: ImageProxy): Bitmap? {
|
||||
Log.d("MatchingMasks", "Here")
|
||||
fun processFrame(
|
||||
imageProxy: ImageProxy,
|
||||
savedMask: Bitmap?,
|
||||
isPortrait: Boolean,
|
||||
thresholdPercent: Int = 75,
|
||||
algorithm: String = HomeActivity.ALGORITHM_HAMMING
|
||||
): Task<SegmentationResult> {
|
||||
val taskCompletionSource = TaskCompletionSource<SegmentationResult>()
|
||||
|
||||
if (!isProcessing.compareAndSet(false, true)) {
|
||||
imageProxy.close()
|
||||
return null
|
||||
taskCompletionSource.setResult(SegmentationResult(null, false))
|
||||
return taskCompletionSource.task
|
||||
}
|
||||
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage == null) {
|
||||
isProcessing.set(false)
|
||||
imageProxy.close()
|
||||
return null
|
||||
taskCompletionSource.setResult(SegmentationResult(null, false))
|
||||
return taskCompletionSource.task
|
||||
}
|
||||
|
||||
val inputImage = InputImage.fromMediaImage(mediaImage, 0)
|
||||
var bitmapMask: Bitmap? = null
|
||||
|
||||
segmenter.process(inputImage)
|
||||
.addOnSuccessListener(processingExecutor) { result ->
|
||||
var bitmapMask: Bitmap? = null
|
||||
val subject = result.subjects.firstOrNull()
|
||||
val mask = subject?.confidenceMask
|
||||
|
||||
|
|
@ -57,9 +75,8 @@ class FrameProcessor(private val onMaskReady: (Bitmap?) -> Unit) {
|
|||
val fullWidth = inputImage.width
|
||||
val fullHeight = inputImage.height
|
||||
|
||||
// Ensure buffer has enough data
|
||||
if (mask.remaining() >= maskWidth * maskHeight) {
|
||||
val colors = IntArray(fullWidth * fullHeight) // Initialized to 0 (TRANSPARENT)
|
||||
val colors = IntArray(fullWidth * fullHeight)
|
||||
mask.rewind()
|
||||
|
||||
for (y in 0 until maskHeight) {
|
||||
|
|
@ -73,24 +90,172 @@ class FrameProcessor(private val onMaskReady: (Bitmap?) -> Unit) {
|
|||
}
|
||||
}
|
||||
}
|
||||
bitmapMask = Bitmap.createBitmap(colors, fullWidth, fullHeight, Bitmap.Config.ARGB_8888)
|
||||
onMaskReady(bitmapMask)
|
||||
|
||||
val rawBitmap = Bitmap.createBitmap(colors, fullWidth, fullHeight, Bitmap.Config.ARGB_8888)
|
||||
|
||||
// Rotate if needed
|
||||
bitmapMask = if (isPortrait) {
|
||||
try {
|
||||
val matrix = Matrix()
|
||||
matrix.postRotate(90f)
|
||||
Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
|
||||
} catch (e: Exception) {
|
||||
Log.e("FrameProcessor", "Error rotating mask", e)
|
||||
rawBitmap
|
||||
}
|
||||
} else {
|
||||
rawBitmap
|
||||
}
|
||||
} else {
|
||||
Log.e("FrameProcessor", "Mask buffer size mismatch. Expected ${maskWidth * maskHeight}, got ${mask.remaining()}")
|
||||
onMaskReady(null)
|
||||
Log.e("FrameProcessor", "Mask buffer size mismatch")
|
||||
}
|
||||
} else {
|
||||
onMaskReady(null)
|
||||
}
|
||||
|
||||
// Calculate match
|
||||
var isMatch = false
|
||||
if (bitmapMask != null && savedMask != null) {
|
||||
// Scale current mask to match saved mask dimensions (640x480) for comparison
|
||||
val comparisonMask = if (bitmapMask.width != savedMask.width || bitmapMask.height != savedMask.height) {
|
||||
try {
|
||||
Bitmap.createScaledBitmap(bitmapMask, savedMask.width, savedMask.height, true)
|
||||
} catch (e: Exception) {
|
||||
Log.e("FrameProcessor", "Error scaling mask for comparison", e)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
bitmapMask
|
||||
}
|
||||
|
||||
if (comparisonMask != null) {
|
||||
isMatch = when (algorithm) {
|
||||
HomeActivity.ALGORITHM_EUCLIDEAN -> calculateEuclideanDistance(savedMask, comparisonMask, thresholdPercent)
|
||||
HomeActivity.ALGORITHM_JACCARD -> calculateJaccardSimilarity(savedMask, comparisonMask, thresholdPercent)
|
||||
else -> calculateHammingDistance(savedMask, comparisonMask, thresholdPercent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
taskCompletionSource.setResult(SegmentationResult(bitmapMask, isMatch))
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e("FrameProcessor", "Subject Segmentation failed", e)
|
||||
onMaskReady(null)
|
||||
taskCompletionSource.setException(e)
|
||||
}
|
||||
.addOnCompleteListener { _ ->
|
||||
isProcessing.set(false)
|
||||
imageProxy.close()
|
||||
}
|
||||
return bitmapMask
|
||||
|
||||
return taskCompletionSource.task
|
||||
}
|
||||
|
||||
private fun calculateHammingDistance(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean {
|
||||
if (mask1.width != mask2.width || mask1.height != mask2.height) {
|
||||
return false
|
||||
}
|
||||
|
||||
val width = mask1.width
|
||||
val height = mask1.height
|
||||
val pixels1 = IntArray(width * height)
|
||||
val pixels2 = IntArray(width * height)
|
||||
|
||||
mask1.getPixels(pixels1, 0, width, 0, 0, width, height)
|
||||
mask2.getPixels(pixels2, 0, width, 0, 0, width, height)
|
||||
|
||||
var distance = 0
|
||||
for (i in pixels1.indices) {
|
||||
val isSet1 = (pixels1[i] ushr 24) > 0
|
||||
val isSet2 = (pixels2[i] ushr 24) > 0
|
||||
|
||||
if (isSet1 != isSet2) {
|
||||
distance++
|
||||
}
|
||||
}
|
||||
|
||||
val totalPixels = width * height
|
||||
val validThreshold = thresholdPercent.coerceIn(1, 100)
|
||||
val allowedDistance = (totalPixels.toLong() * (100 - validThreshold)) / 100
|
||||
|
||||
return distance <= allowedDistance
|
||||
}
|
||||
|
||||
private fun calculateEuclideanDistance(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean {
|
||||
if (mask1.width != mask2.width || mask1.height != mask2.height) {
|
||||
return false
|
||||
}
|
||||
|
||||
val width = mask1.width
|
||||
val height = mask1.height
|
||||
val pixels1 = IntArray(width * height)
|
||||
val pixels2 = IntArray(width * height)
|
||||
|
||||
mask1.getPixels(pixels1, 0, width, 0, 0, width, height)
|
||||
mask2.getPixels(pixels2, 0, width, 0, 0, width, height)
|
||||
|
||||
var sumSq = 0L
|
||||
for (i in pixels1.indices) {
|
||||
// Simple binary comparison for Euclidean distance on masks
|
||||
// Treat existence of pixel as 255, non-existence as 0
|
||||
val val1 = if ((pixels1[i] ushr 24) > 0) 255 else 0
|
||||
val val2 = if ((pixels2[i] ushr 24) > 0) 255 else 0
|
||||
|
||||
val diff = val1 - val2
|
||||
sumSq += diff * diff
|
||||
}
|
||||
|
||||
val euclideanDistance = sqrt(sumSq.toDouble())
|
||||
val maxDistance = sqrt((width * height).toDouble()) * 255.0
|
||||
|
||||
val validThreshold = thresholdPercent.coerceIn(1, 100)
|
||||
val allowedDistance = maxDistance * (100 - validThreshold) / 100.0
|
||||
|
||||
return euclideanDistance <= allowedDistance
|
||||
}
|
||||
|
||||
private fun calculateJaccardSimilarity(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean {
|
||||
if (mask1.width != mask2.width || mask1.height != mask2.height) {
|
||||
return false
|
||||
}
|
||||
|
||||
val width = mask1.width
|
||||
val height = mask1.height
|
||||
val pixels1 = IntArray(width * height)
|
||||
val pixels2 = IntArray(width * height)
|
||||
|
||||
mask1.getPixels(pixels1, 0, width, 0, 0, width, height)
|
||||
mask2.getPixels(pixels2, 0, width, 0, 0, width, height)
|
||||
|
||||
var intersection = 0
|
||||
var union = 0
|
||||
|
||||
for (i in pixels1.indices) {
|
||||
// mask1 is Saved Mask (Inverse: Subject is Transparent/0, Background is Black/255)
|
||||
// NOTE: Wait, the saved mask was inverted originally?
|
||||
// Let's stick to generic "isSet" logic.
|
||||
// In previous turns, I assumed mask1 was "Inverse" and mask2 was "Normal".
|
||||
// But for general algorithms, let's normalize.
|
||||
// In CameraProcessor, we load the saved mask. It is likely the inverted one.
|
||||
// Let's assume:
|
||||
// Saved Mask (mask1): Alpha < 128 means subject (because it's inverted)
|
||||
// Current Mask (mask2): Alpha > 0 means subject
|
||||
|
||||
val alpha1 = (pixels1[i] ushr 24) and 0xFF
|
||||
val isSubject1 = alpha1 < 128
|
||||
|
||||
val alpha2 = (pixels2[i] ushr 24) and 0xFF
|
||||
val isSubject2 = alpha2 > 0
|
||||
|
||||
if (isSubject1 && isSubject2) {
|
||||
intersection++
|
||||
}
|
||||
if (isSubject1 || isSubject2) {
|
||||
union++
|
||||
}
|
||||
}
|
||||
|
||||
if (union == 0) return false
|
||||
|
||||
val jaccardIndex = (intersection.toDouble() / union.toDouble()) * 100
|
||||
return jaccardIndex >= thresholdPercent
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,82 @@ package com.example.animalrating
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class HomeActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
const val ALGORITHM_HAMMING = "Hamming Distance"
|
||||
const val ALGORITHM_EUCLIDEAN = "Euclidean Distance"
|
||||
const val ALGORITHM_JACCARD = "Jaccard Similarity"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_home)
|
||||
|
||||
setupUI()
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
// Navigation buttons
|
||||
findViewById<Button>(R.id.btnViewGallery).setOnClickListener {
|
||||
startActivity(Intent(this, GalleryActivity::class.java))
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.btnSelectCow).setOnClickListener {
|
||||
startActivity(Intent(this, CowSelectionActivity::class.java))
|
||||
saveSettingsAndStart()
|
||||
}
|
||||
|
||||
// Algorithm Spinner
|
||||
val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm)
|
||||
val algorithms = listOf(ALGORITHM_HAMMING, ALGORITHM_EUCLIDEAN, ALGORITHM_JACCARD)
|
||||
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, algorithms)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner.adapter = adapter
|
||||
|
||||
// Set default selection from preferences or intent
|
||||
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
|
||||
val savedAlg = prefs.getString("ALGORITHM", ALGORITHM_HAMMING)
|
||||
spinner.setSelection(algorithms.indexOf(savedAlg))
|
||||
|
||||
// Threshold SeekBar
|
||||
val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold)
|
||||
val tvThreshold = findViewById<TextView>(R.id.tvThresholdValue)
|
||||
|
||||
val savedThreshold = prefs.getInt("THRESHOLD", 75)
|
||||
seekBar.progress = savedThreshold
|
||||
tvThreshold.text = "$savedThreshold%"
|
||||
|
||||
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
tvThreshold.text = "$progress%"
|
||||
}
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSettingsAndStart() {
|
||||
val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm)
|
||||
val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold)
|
||||
|
||||
val selectedAlgorithm = spinner.selectedItem.toString()
|
||||
val threshold = seekBar.progress
|
||||
|
||||
// Save to preferences
|
||||
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
|
||||
prefs.edit().apply {
|
||||
putString("ALGORITHM", selectedAlgorithm)
|
||||
putInt("THRESHOLD", threshold)
|
||||
apply()
|
||||
}
|
||||
|
||||
startActivity(Intent(this, CowSelectionActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#333333"
|
||||
android:layout_marginBottom="48dp"/>
|
||||
android:layout_marginBottom="32dp"/>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -30,6 +30,64 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Settings"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Algorithm:"
|
||||
android:layout_marginEnd="8dp"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerAlgorithm"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Threshold: "/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvThresholdValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="75%"/>
|
||||
</LinearLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seekBarThreshold"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="100"
|
||||
android:progress="75"/>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -29,12 +29,4 @@
|
|||
android:alpha="0.9"
|
||||
android:elevation="10dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Save"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginBottom="32dp"/>
|
||||
|
||||
</FrameLayout>
|
||||
Loading…
Reference in New Issue