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
|
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
|
val isPortrait = (orientation == "front" || orientation == "back")
|
||||||
Log.d("MatchingMasks", "Comparing masks")
|
|
||||||
if (calculateHammingDistance(savedMask, current, threshold)) {
|
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()
|
takePhoto()
|
||||||
} else {
|
}
|
||||||
Toast.makeText(this, "Masks do not match. Please align better.", Toast.LENGTH_SHORT).show()
|
}
|
||||||
}
|
.addOnFailureListener { e ->
|
||||||
|
Log.e("CameraProcessor", "Frame processing error", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
} else {
|
|
||||||
Log.e("FrameProcessor", "Mask buffer size mismatch. Expected ${maskWidth * maskHeight}, got ${mask.remaining()}")
|
// Rotate if needed
|
||||||
onMaskReady(null)
|
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 {
|
} else {
|
||||||
onMaskReady(null)
|
rawBitmap
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("FrameProcessor", "Mask buffer size mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
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))
|
startActivity(Intent(this, CowSelectionActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue