P6 parity probe: same photo through legacy Skia + native engines, 3-panel composite+diff on Xiaomi

This commit is contained in:
2026-09-09 17:06:54 +07:00
parent a51b28f32a
commit 1fdf765d54
3 changed files with 199 additions and 0 deletions
@@ -7,6 +7,7 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.util.Base64
@@ -215,6 +216,14 @@ class RecipescamExportModule : Module() {
}
srcFile.absolutePath
}
// P6 parity probe helper: downscale-decode the two engine outputs (legacy
// Skia vs native) and emit a 3-panel composite [legacy|native|diff(x8)]
// JPEG plus diff stats — probe-only, removed at P7.
AsyncFunction("parityCompareAsync") Coroutine { pathA: String, pathB: String, outPath: String, maxDim: Int ->
val ctx = appContext.reactContext ?: error("react context lost")
withContext(Dispatchers.IO) { parityCompare(ctx, pathA, pathB, outPath, maxDim) }
}
}
}
@@ -1058,3 +1067,76 @@ private fun stampGeo(
}
return Triple(diff, amber, white)
}
// P6 parity helper: decode both outputs downscaled to maxDim (power-of-two
// inSampleSize — both files land on the same grid), compute per-pixel abs
// diff on RGB, and write a 3-panel composite JPEG [A | B | diff*8 grayscale].
// Returns diff stats for the JS log. Probe-only, removed at P7.
private fun decodeDownscaled(path: String, maxDim: Int): Bitmap {
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(path, bounds)
check(bounds.outWidth > 0 && bounds.outHeight > 0) { "decode bounds failed $path" }
var sample = 1
val dim = max(bounds.outWidth, bounds.outHeight)
while (dim / (sample * 2) >= maxDim) sample *= 2
val opts = BitmapFactory.Options().apply { inSampleSize = sample }
return BitmapFactory.decodeFile(path, opts) ?: error("decode failed $path")
}
private fun parityCompare(ctx: Context, pathA: String, pathB: String, outPath: String, maxDim: Int): Map<String, Any> {
val a = decodeDownscaled(pathA, maxDim)
val b = decodeDownscaled(pathB, maxDim)
val aW = a.width; val aH = a.height
val bW = b.width; val bH = b.height
val w = min(aW, bW)
val h = min(aH, bH)
val pa = IntArray(aW * aH).also { a.getPixels(it, 0, aW, 0, 0, aW, aH) }
val pb = IntArray(bW * bH).also { b.getPixels(it, 0, bW, 0, 0, bW, bH) }
var sum = 0L
var maxAbs = 0
val gt = IntArray(3)
val diffColors = IntArray(w * h)
for (y in 0 until h) {
val rowA = y * aW
val rowB = y * bW
val rowD = y * w
for (x in 0 until w) {
val ca = pa[rowA + x]
val cb = pb[rowB + x]
val dR = abs(((ca shr 16) and 0xFF) - ((cb shr 16) and 0xFF))
val dG = abs(((ca shr 8) and 0xFF) - ((cb shr 8) and 0xFF))
val dB = abs((ca and 0xFF) - (cb and 0xFF))
val mx = max(dR, max(dG, dB)); if (mx > maxAbs) maxAbs = mx
val avg = (dR + dG + dB) / 3
sum += dR + dG + dB
if (avg > 3) gt[0]++; if (avg > 12) gt[1]++; if (avg > 32) gt[2]++
val amp = min(avg * 8, 255)
diffColors[rowD + x] = (0xFF shl 24) or (amp shl 16) or (amp shl 8) or amp
}
}
val total = w.toLong() * h
val diffBmp = Bitmap.createBitmap(diffColors, w, h, Bitmap.Config.ARGB_8888)
val sep = 8
val outBmp = Bitmap.createBitmap(w * 3 + sep * 2, h, Bitmap.Config.ARGB_8888)
val c = Canvas(outBmp)
c.drawColor(Color.WHITE)
val srcRect = Rect(0, 0, w, h)
c.drawBitmap(a, srcRect, Rect(0, 0, w, h), null)
c.drawBitmap(b, srcRect, Rect(w + sep, 0, w + sep + w, h), null)
c.drawBitmap(diffBmp, srcRect, Rect(2 * (w + sep), 0, 2 * (w + sep) + w, h), null)
FileOutputStream(File(outPath)).use { fos ->
check(outBmp.compress(Bitmap.CompressFormat.JPEG, 90, fos)) { "parity composite encode failed" }
}
a.recycle(); b.recycle(); diffBmp.recycle(); outBmp.recycle()
Log.i(TAG, "parityCompare $pathA vs $pathB -> $outPath panels=${w}x$h total=$total")
return mapOf(
"width" to w, "height" to h,
"aW" to aW, "aH" to aH, "bW" to bW, "bH" to bH,
"meanAbs" to (sum / (total * 3.0)),
"maxAbs" to maxAbs,
"gt3pct" to (gt[0] * 100.0 / total),
"gt12pct" to (gt[1] * 100.0 / total),
"gt32pct" to (gt[2] * 100.0 / total),
)
}