Distance and collect images auto

This commit is contained in:
SaiD 2025-11-25 23:06:17 +05:30
parent 3e5a8772d7
commit 6de6ef8bf8
6 changed files with 402 additions and 129 deletions

6
.idea/vcs.xml Normal file
View File

@ -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>

View File

@ -1,14 +1,17 @@
package com.example.animalrating package com.example.animalrating
import android.Manifest import android.Manifest
import android.content.ContentValues
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Color import android.graphics.Color
import android.graphics.Matrix import android.graphics.Matrix
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore
import android.util.Log import android.util.Log
import android.widget.Button import android.util.Size
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts 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.ml.CowAnalyzer
import com.example.animalrating.ui.SilhouetteOverlay import com.example.animalrating.ui.SilhouetteOverlay
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -41,6 +45,9 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
private var orientation: String? = null private var orientation: String? = null
private var currentMask: Bitmap? = null private var currentMask: Bitmap? = null
private var savedMaskBitmap: 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -49,6 +56,11 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
cowName = intent.getStringExtra("COW_NAME") cowName = intent.getStringExtra("COW_NAME")
orientation = intent.getStringExtra("ORIENTATION") 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 // Set orientation based on selected view
if (orientation == "front" || orientation == "back") { if (orientation == "front" || orientation == "back") {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
@ -61,33 +73,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
segmentationOverlay = findViewById(R.id.segmentationOverlay) segmentationOverlay = findViewById(R.id.segmentationOverlay)
savedMaskOverlay = findViewById(R.id.savedMaskOverlay) savedMaskOverlay = findViewById(R.id.savedMaskOverlay)
frameProcessor = FrameProcessor { maskBitmap -> frameProcessor = FrameProcessor()
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()
}
val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0) val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0)
overlay.setSilhouette(silhouetteId) overlay.setSilhouette(silhouetteId)
@ -98,6 +84,9 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
} }
private fun takePhoto() { private fun takePhoto() {
if (isPhotoTaken) return
isPhotoTaken = true
val imageCapture = imageCapture ?: return val imageCapture = imageCapture ?: return
val name = cowName ?: "unknown" val name = cowName ?: "unknown"
@ -121,6 +110,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
override fun onError(exc: ImageCaptureException) { override fun onError(exc: ImageCaptureException) {
Log.e("MainActivity", "Photo capture failed: ${exc.message}", exc) Log.e("MainActivity", "Photo capture failed: ${exc.message}", exc)
Toast.makeText(baseContext, "Photo capture failed", Toast.LENGTH_SHORT).show() Toast.makeText(baseContext, "Photo capture failed", Toast.LENGTH_SHORT).show()
isPhotoTaken = false
} }
override fun onImageSaved(output: ImageCapture.OutputFileResults) { override fun onImageSaved(output: ImageCapture.OutputFileResults) {
@ -139,19 +129,58 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
} }
} }
// Save to public gallery
saveToGallery(file)
val msg = "Saved as $filename" val msg = "Saved as $filename"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
finish() finish()
} catch (e: Exception) { } 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() 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() { private fun loadSavedMask() {
val side = orientation ?: "unknown" val side = orientation ?: "unknown"
val filename = "${side}_mask.png" val filename = "${side}_mask.png"
@ -160,12 +189,19 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
if (file.exists()) { if (file.exists()) {
try { try {
val savedBitmap = BitmapFactory.decodeFile(file.absolutePath) 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) val greenMask = applyGreenColor(savedBitmap)
savedMaskOverlay.setImageBitmap(greenMask) savedMaskOverlay.setImageBitmap(greenMask)
savedMaskOverlay.alpha = 0.5f 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) { } catch (e: Exception) {
Log.e("CameraProcessor", "Error loading saved mask", e) Log.e("CameraProcessor", "Error loading saved mask", e)
} }
@ -181,69 +217,13 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
for (i in pixels.indices) { for (i in pixels.indices) {
val alpha = (pixels[i] shr 24) and 0xff val alpha = (pixels[i] shr 24) and 0xff
if (alpha > 10) { 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) pixels[i] = Color.argb(alpha, 0, 255, 0)
} }
} }
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888) 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 = private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) startCamera() if (granted) startCamera()
@ -256,13 +236,14 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
val cameraProvider = providerFuture.get() val cameraProvider = providerFuture.get()
val preview = androidx.camera.core.Preview.Builder().build() val preview = androidx.camera.core.Preview.Builder().build()
preview.setSurfaceProvider(previewView.surfaceProvider) preview.surfaceProvider = previewView.surfaceProvider
imageCapture = ImageCapture.Builder() imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build() .build()
val analyzer = ImageAnalysis.Builder() val analyzer = ImageAnalysis.Builder()
.setTargetResolution(Size(640, 480))
.setBackpressureStrategy( .setBackpressureStrategy(
ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
) )
@ -284,18 +265,27 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
} }
override fun onFrame(imageProxy: ImageProxy) { override fun onFrame(imageProxy: ImageProxy) {
val current = frameProcessor.processFrame(imageProxy) if (isPhotoTaken) {
val savedMask = savedMaskBitmap imageProxy.close()
val threshold = 80000 return
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()
}
} }
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)
}
} }
} }

View File

@ -2,16 +2,25 @@ package com.example.animalrating
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.Matrix
import android.util.Log import android.util.Log
import androidx.camera.core.ExperimentalGetImage import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageProxy 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.common.InputImage
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean 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 isProcessing = AtomicBoolean(false)
private val processingExecutor = Executors.newSingleThreadExecutor() private val processingExecutor = Executors.newSingleThreadExecutor()
@ -26,25 +35,34 @@ class FrameProcessor(private val onMaskReady: (Bitmap?) -> Unit) {
private val segmenter = SubjectSegmentation.getClient(options) private val segmenter = SubjectSegmentation.getClient(options)
@ExperimentalGetImage @ExperimentalGetImage
fun processFrame(imageProxy: ImageProxy): Bitmap? { fun processFrame(
Log.d("MatchingMasks", "Here") 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)) { if (!isProcessing.compareAndSet(false, true)) {
imageProxy.close() imageProxy.close()
return null taskCompletionSource.setResult(SegmentationResult(null, false))
return taskCompletionSource.task
} }
val mediaImage = imageProxy.image val mediaImage = imageProxy.image
if (mediaImage == null) { if (mediaImage == null) {
isProcessing.set(false) isProcessing.set(false)
imageProxy.close() imageProxy.close()
return null taskCompletionSource.setResult(SegmentationResult(null, false))
return taskCompletionSource.task
} }
val inputImage = InputImage.fromMediaImage(mediaImage, 0) val inputImage = InputImage.fromMediaImage(mediaImage, 0)
var bitmapMask: Bitmap? = null
segmenter.process(inputImage) segmenter.process(inputImage)
.addOnSuccessListener(processingExecutor) { result -> .addOnSuccessListener(processingExecutor) { result ->
var bitmapMask: Bitmap? = null
val subject = result.subjects.firstOrNull() val subject = result.subjects.firstOrNull()
val mask = subject?.confidenceMask val mask = subject?.confidenceMask
@ -57,9 +75,8 @@ class FrameProcessor(private val onMaskReady: (Bitmap?) -> Unit) {
val fullWidth = inputImage.width val fullWidth = inputImage.width
val fullHeight = inputImage.height val fullHeight = inputImage.height
// Ensure buffer has enough data
if (mask.remaining() >= maskWidth * maskHeight) { if (mask.remaining() >= maskWidth * maskHeight) {
val colors = IntArray(fullWidth * fullHeight) // Initialized to 0 (TRANSPARENT) val colors = IntArray(fullWidth * fullHeight)
mask.rewind() mask.rewind()
for (y in 0 until maskHeight) { 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 { } else {
Log.e("FrameProcessor", "Mask buffer size mismatch. Expected ${maskWidth * maskHeight}, got ${mask.remaining()}") Log.e("FrameProcessor", "Mask buffer size mismatch")
onMaskReady(null)
} }
} 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 -> .addOnFailureListener { e ->
Log.e("FrameProcessor", "Subject Segmentation failed", e) Log.e("FrameProcessor", "Subject Segmentation failed", e)
onMaskReady(null) taskCompletionSource.setException(e)
} }
.addOnCompleteListener { _ -> .addOnCompleteListener { _ ->
isProcessing.set(false) isProcessing.set(false)
imageProxy.close() 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
} }
} }

View File

@ -2,20 +2,82 @@ package com.example.animalrating
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
import android.widget.SeekBar
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
class HomeActivity : 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home) setContentView(R.layout.activity_home)
setupUI()
}
private fun setupUI() {
// Navigation buttons
findViewById<Button>(R.id.btnViewGallery).setOnClickListener { findViewById<Button>(R.id.btnViewGallery).setOnClickListener {
startActivity(Intent(this, GalleryActivity::class.java)) startActivity(Intent(this, GalleryActivity::class.java))
} }
findViewById<Button>(R.id.btnSelectCow).setOnClickListener { 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))
} }
} }

View File

@ -15,7 +15,7 @@
android:textSize="32sp" android:textSize="32sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="#333333" android:textColor="#333333"
android:layout_marginBottom="48dp"/> android:layout_marginBottom="32dp"/>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -31,6 +31,64 @@
android:orientation="vertical" android:orientation="vertical"
android:gravity="center"> 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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -29,12 +29,4 @@
android:alpha="0.9" android:alpha="0.9"
android:elevation="10dp"/> 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> </FrameLayout>