Add live edge-detection overlay while aiming; v0.2.0
All checks were successful
Build APK / build (push) Successful in 6m9s
All checks were successful
Build APK / build (push) Successful in 6m9s
Bind a CameraX ImageAnalysis use case that runs EdgeDetector on downscaled YUV frames (throttled ~6/s) and draw the detected document quad as a live green overlay on the preview, mapped through the FILL_CENTER crop. Preview switched to TextureView (COMPATIBLE) mode so the Compose overlay renders on top. Bump versionCode 4 / versionName 0.2.0. Fixes #4 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package eu.geyskens.pdfscan.scan
|
||||
|
||||
import android.graphics.PointF
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import org.opencv.core.Core
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
|
||||
/**
|
||||
* Runs [EdgeDetector] on live camera frames (throttled) and reports the detected document
|
||||
* corners, normalized to a display-oriented frame, together with that frame's aspect ratio
|
||||
* (width / height) so the overlay can map them onto the preview.
|
||||
*/
|
||||
class DocumentAnalyzer(
|
||||
private val onResult: (corners: List<PointF>?, frameAspect: Float) -> Unit,
|
||||
) : ImageAnalysis.Analyzer {
|
||||
|
||||
@Volatile
|
||||
private var lastRun = 0L
|
||||
|
||||
override fun analyze(image: ImageProxy) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastRun < THROTTLE_MS) {
|
||||
image.close()
|
||||
return
|
||||
}
|
||||
lastRun = now
|
||||
val result = try {
|
||||
detect(image)
|
||||
} catch (e: Throwable) {
|
||||
android.util.Log.w("DocAnalyzer", "detect failed", e)
|
||||
null
|
||||
}
|
||||
image.close()
|
||||
if (result != null) onResult(result.first, result.second)
|
||||
}
|
||||
|
||||
private fun detect(image: ImageProxy): Pair<List<PointF>?, Float> {
|
||||
// The Y plane of the YUV frame is already a grayscale image.
|
||||
val plane = image.planes[0]
|
||||
val rowStride = plane.rowStride
|
||||
val buffer = plane.buffer
|
||||
val out = ByteArray(image.width * image.height)
|
||||
for (row in 0 until image.height) {
|
||||
buffer.position(row * rowStride)
|
||||
buffer.get(out, row * image.width, image.width)
|
||||
}
|
||||
val gray = Mat(image.height, image.width, CvType.CV_8UC1)
|
||||
gray.put(0, 0, out)
|
||||
|
||||
// Rotate the sensor-oriented frame to match how the preview is displayed.
|
||||
when (image.imageInfo.rotationDegrees) {
|
||||
90 -> Core.rotate(gray, gray, Core.ROTATE_90_CLOCKWISE)
|
||||
180 -> Core.rotate(gray, gray, Core.ROTATE_180)
|
||||
270 -> Core.rotate(gray, gray, Core.ROTATE_90_COUNTERCLOCKWISE)
|
||||
}
|
||||
val aspect = gray.cols().toFloat() / gray.rows()
|
||||
return EdgeDetector.detectFromGray(gray) to aspect // detectFromGray releases gray
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val THROTTLE_MS = 150L
|
||||
}
|
||||
}
|
||||
@@ -22,17 +22,25 @@ object EdgeDetector {
|
||||
fun detect(bitmap: Bitmap): List<PointF>? {
|
||||
val src = Mat()
|
||||
Utils.bitmapToMat(bitmap, src)
|
||||
try {
|
||||
val scale = PROCESS_WIDTH / src.cols()
|
||||
val work = Mat()
|
||||
Imgproc.resize(src, work, Size(PROCESS_WIDTH, src.rows() * scale))
|
||||
val gray = Mat()
|
||||
Imgproc.cvtColor(src, gray, Imgproc.COLOR_RGBA2GRAY)
|
||||
src.release()
|
||||
return detectFromGray(gray)
|
||||
}
|
||||
|
||||
val gray = Mat()
|
||||
Imgproc.cvtColor(work, gray, Imgproc.COLOR_RGBA2GRAY)
|
||||
Imgproc.GaussianBlur(gray, gray, Size(5.0, 5.0), 0.0)
|
||||
/**
|
||||
* Detects on a full-resolution single-channel grayscale [gray] Mat (e.g. a camera
|
||||
* frame's Y plane). Corners are normalized to [gray]'s dimensions. Releases [gray].
|
||||
*/
|
||||
fun detectFromGray(gray: Mat): List<PointF>? {
|
||||
try {
|
||||
val scale = PROCESS_WIDTH / gray.cols()
|
||||
val work = Mat()
|
||||
Imgproc.resize(gray, work, Size(PROCESS_WIDTH, gray.rows() * scale))
|
||||
Imgproc.GaussianBlur(work, work, Size(5.0, 5.0), 0.0)
|
||||
|
||||
val edges = Mat()
|
||||
Imgproc.Canny(gray, edges, 50.0, 150.0)
|
||||
Imgproc.Canny(work, edges, 50.0, 150.0)
|
||||
// Close small gaps so contours form a continuous loop.
|
||||
val kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(5.0, 5.0))
|
||||
Imgproc.dilate(edges, edges, kernel)
|
||||
@@ -65,7 +73,7 @@ object EdgeDetector {
|
||||
}
|
||||
return best
|
||||
} finally {
|
||||
src.release()
|
||||
gray.release()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user