Every document scanner app has the same problem: users tap the capture button while their hand is still moving, and the scan comes out blurry.
I spent two weeks solving this while building a production Android scanner app (21K lines of Kotlin). Here's the approach that actually works.
The Problem
CameraX gives you a camera preview, but it has no concept of "is the frame stable enough to capture?" You need to build that yourself.
The Solution: RMS-Based Frame Stability
Instead of complex optical flow or gyroscope fusion, I used a simple but effective approach: compare consecutive frames using Root Mean Square (RMS) difference.
class FrameStabilityAnalyzer : ImageAnalysis.Analyzer {
private var lastFrameBytes: ByteArray? = null
private var isStable = false
private val STABILITY_THRESHOLD = 5.0 // Tune this value
override fun analyze(imageProxy: ImageProxy) {
val currentBytes = imageProxy.planes[0].buffer.toByteArray()
lastFrameBytes?.let { previous ->
val rms = calculateRMS(previous, currentBytes)
isStable = rms < STABILITY_THRESHOLD
}
lastFrameBytes = currentBytes
imageProxy.close()
}
private fun calculateRMS(a: ByteArray, b: ByteArray): Double {
val size = minOf(a.size, b.size)
var sumSquares = 0.0
for (i in 0 until size) {
val diff = (a[i].toInt() and 0xFF) - (b[i].toInt() and 0xFF)
sumSquares += diff * diff
}
return sqrt(sumSquares / size)
}
}
How It Works
- Grab the luminance plane from each camera frame (Y plane in YUV)
- Compare it to the previous frame pixel by pixel
- Calculate the RMS difference -- low values mean the camera isn't moving
-
Gate the capture button -- only allow capture when
isStable == true
The Key Insight
The threshold value (STABILITY_THRESHOLD = 5.0) is critical. Too low and the user can never capture. Too high and you still get blurry scans.
I found 5.0 works well for document scanning where you want sharp text. For general photos, you might go up to 8-10.
Wiring It Up with CameraX
val stabilityAnalyzer = FrameStabilityAnalyzer()
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { it.setAnalyzer(executor, stabilityAnalyzer) }
// In your UI, observe stabilityAnalyzer.isStable
// Show a green border when stable, red when shaking
Pro Tips
- Only analyze a downsampled version of the frame for performance. You don't need full resolution to detect motion.
- Add a debounce -- require 3 consecutive stable frames before allowing capture. This prevents false positives.
- Show visual feedback -- users need to know WHY the button is grayed out. A subtle "hold steady" indicator works better than nothing.
Results
After implementing this, the percentage of blurry scans in my app dropped by roughly 80%. Users don't even notice the gating -- they naturally hold still for a fraction of a second, and the capture fires automatically.
This is one of those details that separates a good scanner from a great one. If you're building anything with CameraX that involves document capture, frame stability detection is worth the effort.
I'm currently building this as part of a full production scanner app template. If you're interested in skipping the months of work, check out my profile for more details.
Top comments (0)