chore: import upstream snapshot with attribution
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.benchmark
|
||||
|
||||
object BenchmarkFixtures {
|
||||
const val defaultReferenceImageName = "android_ocr_benchmark_reference.jpg"
|
||||
}
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.benchmark
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Debug
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.paddle.ocr.EngineConfig
|
||||
import com.paddle.ocr.PaddleOCR
|
||||
import com.paddle.ocr.PaddleOCRConfig
|
||||
import com.paddle.ocr.model.OCRRunResult
|
||||
import com.paddle.ocr.util.OpenCVUtils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class OCRBenchmarkTest {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "OCRBenchmark"
|
||||
|
||||
// Instrumentation arguments (passed via -Pandroid.testInstrumentationRunnerArguments.*)
|
||||
private const val ARG_WARMUP = "warmup"
|
||||
private const val ARG_ITERATIONS = "iterations"
|
||||
private const val ARG_REC_BATCH_SIZE = "rec_batch_size"
|
||||
|
||||
private const val DEFAULT_WARMUP = 3
|
||||
private const val DEFAULT_ITERATIONS = 10
|
||||
|
||||
private lateinit var ocr: PaddleOCR
|
||||
private lateinit var outputDir: File
|
||||
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
fun setUp() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
OpenCVUtils.init(context)
|
||||
|
||||
outputDir = File(context.getExternalFilesDir(null), "ocr_benchmark")
|
||||
outputDir.mkdirs()
|
||||
println("[OCRBenchmark] Output dir: ${outputDir.absolutePath}")
|
||||
|
||||
runBlocking {
|
||||
val batchSize = getInstrumentationArgument(ARG_REC_BATCH_SIZE)?.toIntOrNull() ?: 1
|
||||
val config = PaddleOCRConfig(recBatchSize = batchSize)
|
||||
ocr = PaddleOCR.create(context, config, EngineConfig())
|
||||
}
|
||||
println("[OCRBenchmark] Engine ready, coldLoadTimeMs=${ocr.coldLoadTimeMs}")
|
||||
}
|
||||
|
||||
private fun getInstrumentationArgument(key: String): String? {
|
||||
val args = InstrumentationRegistry.getArguments()
|
||||
return args[key] as? String
|
||||
}
|
||||
|
||||
private fun saveAndPrint(filename: String, json: JSONObject) {
|
||||
val file = File(outputDir, filename)
|
||||
file.writeText(json.toString(2))
|
||||
println("[OCRBenchmark] Saved: ${file.absolutePath}")
|
||||
println("[OCRBenchmark] ---- $filename ----")
|
||||
json.toString(2).lines().forEach { println("[OCRBenchmark] $it") }
|
||||
println("[OCRBenchmark] ---- end ----")
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Accuracy
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun testOCRExportJSON() {
|
||||
val imageBytes = loadBenchmarkImageBytes()
|
||||
val result = runBlocking { ocr.recognize(imageBytes) }
|
||||
val payload = buildExportPayload(result)
|
||||
saveAndPrint("accuracy_export.json", payload)
|
||||
|
||||
assertTrue("Should have results", payload.getJSONArray("items").length() > 0)
|
||||
assertTrue("Should detect text", result.lineCount > 0)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Latency
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun testLatencyBenchmark() {
|
||||
val imageBytes = loadBenchmarkImageBytes()
|
||||
val warmup = getInstrumentationArgument(ARG_WARMUP)?.toIntOrNull() ?: DEFAULT_WARMUP
|
||||
val iterations = getInstrumentationArgument(ARG_ITERATIONS)?.toIntOrNull() ?: DEFAULT_ITERATIONS
|
||||
|
||||
println("[OCRBenchmark] Warming up $warmup iterations...")
|
||||
runBlocking { repeat(warmup) { ocr.recognize(imageBytes) } }
|
||||
|
||||
println("[OCRBenchmark] Measuring $iterations iterations...")
|
||||
val totals = mutableListOf<Long>()
|
||||
val dets = mutableListOf<Long>()
|
||||
val detPre = mutableListOf<Long>()
|
||||
val detInf = mutableListOf<Long>()
|
||||
val detPost = mutableListOf<Long>()
|
||||
val recs = mutableListOf<Long>()
|
||||
val recPre = mutableListOf<Long>()
|
||||
val recInf = mutableListOf<Long>()
|
||||
val recPost = mutableListOf<Long>()
|
||||
val overheads = mutableListOf<Long>()
|
||||
val perLineMs = mutableListOf<Long>()
|
||||
val detShapes = mutableListOf<List<Int>>()
|
||||
val recShapes = mutableListOf<List<Int>>()
|
||||
var firstLineCount = 0
|
||||
|
||||
runBlocking {
|
||||
repeat(iterations) { i ->
|
||||
val r = ocr.recognize(imageBytes)
|
||||
if (i == 0) firstLineCount = r.lineCount
|
||||
totals.add(r.totalTimeMs)
|
||||
dets.add(r.detectionTimeMs)
|
||||
detPre.add(r.detPreprocessMs)
|
||||
detInf.add(r.detInferenceMs)
|
||||
detPost.add(r.detPostprocessMs)
|
||||
recs.add(r.recognitionTimeMs)
|
||||
recPre.add(r.recPreprocessMs)
|
||||
recInf.add(r.recInferenceMs)
|
||||
recPost.add(r.recPostprocessMs)
|
||||
overheads.add(r.pipelineOverheadMs)
|
||||
detShapes.add(r.detInputShape)
|
||||
recShapes.addAll(r.recInputShapes)
|
||||
perLineMs.addAll(r.perLineRecMs)
|
||||
if ((i + 1) % 5 == 0) println("[OCRBenchmark] $i/${iterations}...")
|
||||
}
|
||||
}
|
||||
|
||||
val memInfo = Debug.MemoryInfo()
|
||||
Debug.getMemoryInfo(memInfo)
|
||||
|
||||
val payload = JSONObject().apply {
|
||||
put("schemaVersion", 1)
|
||||
put("deviceModel", Build.MODEL)
|
||||
put("osVersion", Build.VERSION.RELEASE)
|
||||
put("ortExecutionProvider", "CPU")
|
||||
put("inputImage", BenchmarkFixtures.defaultReferenceImageName)
|
||||
put("inputBytes", imageBytes.size)
|
||||
put("warmupIterations", warmup)
|
||||
put("measuredIterations", iterations)
|
||||
put("firstMeasuredLineCount", firstLineCount)
|
||||
put("coldLoadTimeMs", ocr.coldLoadTimeMs)
|
||||
if (totals.isNotEmpty()) {
|
||||
put("firstMeasuredRun", JSONObject().apply {
|
||||
put("totalTimeMs", totals.first())
|
||||
put("detectionTimeMs", dets.first())
|
||||
put("recognitionTimeMs", recs.first())
|
||||
put("pipelineOverheadTimeMs", overheads.first())
|
||||
})
|
||||
}
|
||||
put("totalTimeMs", summarizeMs(totals).toJson())
|
||||
put("detectionTimeMs", summarizeMs(dets).toJson())
|
||||
put("detectionPreprocessTimeMs", summarizeMs(detPre).toJson())
|
||||
put("detectionInferenceTimeMs", summarizeMs(detInf).toJson())
|
||||
put("detectionPostprocessTimeMs", summarizeMs(detPost).toJson())
|
||||
put("recognitionTimeMs", summarizeMs(recs).toJson())
|
||||
put("recognitionPreprocessTimeMs", summarizeMs(recPre).toJson())
|
||||
put("recognitionInferenceTimeMs", summarizeMs(recInf).toJson())
|
||||
put("recognitionPostprocessTimeMs", summarizeMs(recPost).toJson())
|
||||
put("pipelineOverheadTimeMs", summarizeMs(overheads).toJson())
|
||||
put("memoryPssKb", memInfo.totalPss)
|
||||
put("inputShapeDistribution", JSONObject().apply {
|
||||
put("detection", summarizeShapes(detShapes))
|
||||
put("recognition", summarizeShapes(recShapes))
|
||||
})
|
||||
if (perLineMs.isNotEmpty()) {
|
||||
put("recognitionPerLine", JSONObject().apply {
|
||||
put("pooled", JSONObject().apply {
|
||||
put("count", perLineMs.size)
|
||||
put("totalMs", summarizeMs(perLineMs).toJson())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
saveAndPrint("latency_benchmark.json", payload)
|
||||
printSpeedSummary(warmup, iterations, totals, dets, detPre, detInf, detPost, recs, recPre, recInf, recPost, overheads, firstLineCount)
|
||||
|
||||
assertTrue("Should complete all iterations", totals.size == iterations)
|
||||
assertTrue("Should detect text", firstLineCount > 0)
|
||||
}
|
||||
|
||||
private fun printSpeedSummary(
|
||||
warmup: Int,
|
||||
iterations: Int,
|
||||
totals: List<Long>,
|
||||
dets: List<Long>,
|
||||
detPre: List<Long>,
|
||||
detInf: List<Long>,
|
||||
detPost: List<Long>,
|
||||
recs: List<Long>,
|
||||
recPre: List<Long>,
|
||||
recInf: List<Long>,
|
||||
recPost: List<Long>,
|
||||
overheads: List<Long>,
|
||||
lineCount: Int,
|
||||
) {
|
||||
val bar = "+-----------------------------+----------+----------+----------+----------+"
|
||||
val header = "| Stage | Mean ms | Stdev | P90 | Min ms |"
|
||||
fun row(label: String, samples: List<Long>): String {
|
||||
if (samples.isEmpty()) return "| %-27s | %8s | %8s | %8s | %8s |".format(label, "-", "-", "-", "N/A")
|
||||
val mean = samples.map { it.toDouble() }.average()
|
||||
val variance = if (samples.size > 1) samples.map { (it - mean).pow(2) }.average() else 0.0
|
||||
val stdev = sqrt(variance)
|
||||
val sorted = samples.sorted()
|
||||
val p90 = sorted[(0.9 * (sorted.size - 1)).toInt()]
|
||||
val min = sorted.first()
|
||||
return "| %-27s | %8.2f | %8.2f | %8d | %8d |".format(label, mean, stdev, p90, min)
|
||||
}
|
||||
|
||||
println("[OCRBenchmark] ")
|
||||
println("[OCRBenchmark] ╔════════════════════════════════════════════════════════════════════════════════╗")
|
||||
println("[OCRBenchmark] ║ PP-OCRv6 Speed Benchmark Results ║")
|
||||
println("[OCRBenchmark] ╠════════════════════════════════════════════════════════════════════════════════╣")
|
||||
println("[OCRBenchmark] ║ Device: ${Build.MODEL} | OS: Android ${Build.VERSION.RELEASE} | Lines: $lineCount")
|
||||
println("[OCRBenchmark] ║ Cold load: ${ocr.coldLoadTimeMs}ms | Warmup: $warmup | Measured: $iterations")
|
||||
println("[OCRBenchmark] ╠════════════════════════════════════════════════════════════════════════════════╣")
|
||||
println("[OCRBenchmark] $bar")
|
||||
println("[OCRBenchmark] $header")
|
||||
println("[OCRBenchmark] $bar")
|
||||
println("[OCRBenchmark] ${row("Total pipeline", totals)}")
|
||||
println("[OCRBenchmark] $bar")
|
||||
println("[OCRBenchmark] ${row(" Detection (total)", dets)}")
|
||||
println("[OCRBenchmark] ${row(" Preprocess", detPre)}")
|
||||
println("[OCRBenchmark] ${row(" Inference", detInf)}")
|
||||
println("[OCRBenchmark] ${row(" Postprocess", detPost)}")
|
||||
println("[OCRBenchmark] ${row(" Recognition (total)", recs)}")
|
||||
println("[OCRBenchmark] ${row(" Preprocess", recPre)}")
|
||||
println("[OCRBenchmark] ${row(" Inference", recInf)}")
|
||||
println("[OCRBenchmark] ${row(" Postprocess", recPost)}")
|
||||
println("[OCRBenchmark] ${row(" Pipeline overhead", overheads)}")
|
||||
println("[OCRBenchmark] $bar")
|
||||
println("[OCRBenchmark] ╚════════════════════════════════════════════════════════════════════════════════╝")
|
||||
println("[OCRBenchmark] ")
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Memory
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun testMemoryBenchmark() {
|
||||
val imageBytes = loadBenchmarkImageBytes()
|
||||
val warmup = getInstrumentationArgument(ARG_WARMUP)?.toIntOrNull() ?: DEFAULT_WARMUP
|
||||
val iterations = getInstrumentationArgument(ARG_ITERATIONS)?.toIntOrNull() ?: DEFAULT_ITERATIONS
|
||||
|
||||
runBlocking { repeat(warmup) { ocr.recognize(imageBytes) } }
|
||||
Runtime.getRuntime().gc()
|
||||
Thread.sleep(500)
|
||||
|
||||
val before = Debug.MemoryInfo()
|
||||
Debug.getMemoryInfo(before)
|
||||
|
||||
runBlocking { repeat(iterations) { ocr.recognize(imageBytes) } }
|
||||
|
||||
val after = Debug.MemoryInfo()
|
||||
Debug.getMemoryInfo(after)
|
||||
|
||||
val payload = JSONObject().apply {
|
||||
put("schemaVersion", 1)
|
||||
put("deviceModel", Build.MODEL)
|
||||
put("iterations", iterations)
|
||||
put("memoryBeforePssKb", before.totalPss)
|
||||
put("memoryAfterPssKb", after.totalPss)
|
||||
put("memoryDeltaPssKb", after.totalPss - before.totalPss)
|
||||
put("nativeBeforeKb", before.nativePss)
|
||||
put("nativeAfterKb", after.nativePss)
|
||||
put("dalvikBeforeKb", before.dalvikPss)
|
||||
put("dalvikAfterKb", after.dalvikPss)
|
||||
}
|
||||
|
||||
saveAndPrint("memory_benchmark.json", payload)
|
||||
assertTrue("Memory should be measurable", after.totalPss > 0)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Helpers
|
||||
// ========================================================================
|
||||
|
||||
private fun loadBenchmarkImageBytes(): ByteArray {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val stem = BenchmarkFixtures.defaultReferenceImageName.substringBeforeLast(".")
|
||||
val resId = context.resources.getIdentifier(stem, "raw", context.packageName)
|
||||
if (resId == 0) {
|
||||
throw IllegalStateException(
|
||||
"Missing ${BenchmarkFixtures.defaultReferenceImageName} " +
|
||||
"(package=${context.packageName})"
|
||||
)
|
||||
}
|
||||
return context.resources.openRawResource(resId).use { it.readBytes() }
|
||||
}
|
||||
|
||||
data class TimingSummary(val mean: Double, val stdev: Double, val p90: Double) {
|
||||
fun toJson() = JSONObject().apply {
|
||||
put("mean", mean)
|
||||
put("stdev", stdev)
|
||||
put("p90", p90)
|
||||
}
|
||||
}
|
||||
|
||||
private fun summarizeMs(samples: List<Long>): TimingSummary {
|
||||
if (samples.isEmpty()) return TimingSummary(0.0, 0.0, 0.0)
|
||||
val doubles = samples.map { it.toDouble() }
|
||||
val mean = doubles.average()
|
||||
val variance = if (doubles.size > 1)
|
||||
doubles.map { (it - mean).pow(2) }.average()
|
||||
else 0.0
|
||||
val stdev = sqrt(variance)
|
||||
val sorted = doubles.sorted()
|
||||
val p90Idx = (0.9 * (sorted.size - 1)).toInt().coerceIn(0, sorted.size - 1)
|
||||
return TimingSummary(mean, stdev, sorted[p90Idx])
|
||||
}
|
||||
|
||||
private fun summarizeShapes(shapes: List<List<Int>>): JSONArray {
|
||||
val counts = mutableMapOf<String, Int>()
|
||||
shapes.forEach { shape ->
|
||||
val key = shape.joinToString(",")
|
||||
counts[key] = (counts[key] ?: 0) + 1
|
||||
}
|
||||
return JSONArray().apply {
|
||||
counts.entries.sortedByDescending { it.value }.forEach { (shape, count) ->
|
||||
put(JSONObject().apply {
|
||||
put("shape", JSONArray(shape.split(",").map { it.toInt() }))
|
||||
put("count", count)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildExportPayload(result: OCRRunResult): JSONObject {
|
||||
val items = JSONArray().apply {
|
||||
result.results.forEach { r ->
|
||||
put(JSONObject().apply {
|
||||
put("polygon", JSONArray().apply {
|
||||
r.box.points.forEach { pt ->
|
||||
put(JSONArray(listOf(pt.x.toInt(), pt.y.toInt())))
|
||||
}
|
||||
})
|
||||
put("text", r.text)
|
||||
put("score", r.confidence.toDouble())
|
||||
})
|
||||
}
|
||||
}
|
||||
return JSONObject().apply {
|
||||
put("schemaVersion", 1)
|
||||
put("source", "android_ocr_demo")
|
||||
put("items", items)
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 389 KiB |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr
|
||||
|
||||
data class EngineConfig(
|
||||
val numThreads: Int = 4,
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import com.paddle.ocr.engine.OCREngine
|
||||
import com.paddle.ocr.engine.OCREngineResult
|
||||
import com.paddle.ocr.model.OCRRunResult
|
||||
import com.paddle.ocr.model.OCRError
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PaddleOCR private constructor(
|
||||
private val engine: OCREngine,
|
||||
) {
|
||||
/** Time spent loading ONNX models (milliseconds). */
|
||||
val coldLoadTimeMs: Long get() = engine.coldLoadTimeMs
|
||||
|
||||
companion object {
|
||||
|
||||
suspend fun create(context: Context): PaddleOCR {
|
||||
val appContext = context.applicationContext
|
||||
return withContext(Dispatchers.IO) {
|
||||
val engine = OCREngine(appContext, PaddleOCRConfig(), EngineConfig())
|
||||
PaddleOCR(engine)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
context: Context,
|
||||
config: PaddleOCRConfig,
|
||||
engineConfig: EngineConfig = EngineConfig(),
|
||||
): PaddleOCR {
|
||||
val appContext = context.applicationContext
|
||||
return withContext(Dispatchers.IO) {
|
||||
val engine = OCREngine(appContext, config, engineConfig)
|
||||
PaddleOCR(engine)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
context: Context,
|
||||
config: PaddleOCRConfig,
|
||||
engineConfig: EngineConfig,
|
||||
detModelAssetPath: String,
|
||||
recModelAssetPath: String,
|
||||
recConfigAssetPath: String,
|
||||
): PaddleOCR {
|
||||
val appContext = context.applicationContext
|
||||
return withContext(Dispatchers.IO) {
|
||||
val engine = OCREngine(
|
||||
appContext, config, engineConfig,
|
||||
detModelAsset = detModelAssetPath,
|
||||
recModelAsset = recModelAssetPath,
|
||||
recConfigAsset = recConfigAssetPath,
|
||||
)
|
||||
PaddleOCR(engine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun recognize(bitmap: Bitmap): OCRRunResult {
|
||||
if (bitmap.width == 0 || bitmap.height == 0) {
|
||||
throw OCRError.InvalidImage()
|
||||
}
|
||||
return recognizeResult { engine.run(bitmap) }
|
||||
}
|
||||
|
||||
suspend fun recognize(imageBytes: ByteArray): OCRRunResult {
|
||||
if (imageBytes.isEmpty()) {
|
||||
throw OCRError.InvalidImage()
|
||||
}
|
||||
return recognizeResult { engine.run(imageBytes) }
|
||||
}
|
||||
|
||||
private suspend fun recognizeResult(runEngine: () -> OCREngineResult): OCRRunResult {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val result = runEngine()
|
||||
OCRRunResult(
|
||||
results = result.results,
|
||||
detectionTimeMs = result.detectionTimeMs,
|
||||
recognitionTimeMs = result.recognitionTimeMs,
|
||||
totalTimeMs = result.totalTimeMs,
|
||||
lineCount = result.lineCount,
|
||||
detPreprocessMs = result.detPreprocessMs,
|
||||
detInferenceMs = result.detInferenceMs,
|
||||
detPostprocessMs = result.detPostprocessMs,
|
||||
recPreprocessMs = result.recPreprocessMs,
|
||||
recInferenceMs = result.recInferenceMs,
|
||||
recPostprocessMs = result.recPostprocessMs,
|
||||
pipelineOverheadMs = result.pipelineOverheadMs,
|
||||
coldLoadTimeMs = result.coldLoadTimeMs,
|
||||
detInputShape = result.detInputShape,
|
||||
recInputShapes = result.recInputShapes,
|
||||
perLineRecMs = result.perLineRecMs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun release() {
|
||||
withContext(Dispatchers.IO) {
|
||||
engine.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr
|
||||
|
||||
data class PaddleOCRConfig(
|
||||
val detImgMode: String = "BGR",
|
||||
val detLimitSideLen: Int = 64,
|
||||
val detLimitType: String = "min",
|
||||
val detMaxSideLimit: Int = 4000,
|
||||
val detThresh: Float = 0.3f,
|
||||
val detBoxThresh: Float = 0.6f,
|
||||
val detUnclipRatio: Float = 1.5f,
|
||||
val detMaxCandidates: Int = 3000,
|
||||
val detUseDilation: Boolean = false,
|
||||
val detScoreMode: String = "fast",
|
||||
val detBoxType: String = "quad",
|
||||
val recScoreThresh: Float = 0.0f,
|
||||
val recBatchSize: Int = 1,
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.engine
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.paddle.ocr.PaddleOCRConfig
|
||||
import com.paddle.ocr.model.OCRBox
|
||||
import com.paddle.ocr.postprocess.DBPostProcessor
|
||||
import com.paddle.ocr.preprocess.DetPreprocessResult
|
||||
import com.paddle.ocr.preprocess.DetPreprocessor
|
||||
import org.opencv.core.Mat
|
||||
|
||||
class DetectionEngine(
|
||||
private val ortManager: ORTSessionManager,
|
||||
private val config: PaddleOCRConfig,
|
||||
) {
|
||||
data class DetectionResult(
|
||||
val boxes: List<OCRBox>,
|
||||
val preprocessMs: Long,
|
||||
val inferenceMs: Long,
|
||||
val postprocessMs: Long,
|
||||
val timeMs: Long,
|
||||
val inputShape: List<Int>,
|
||||
)
|
||||
|
||||
fun detect(bitmap: Bitmap): DetectionResult {
|
||||
return detect {
|
||||
DetPreprocessor.preprocess(
|
||||
bitmap,
|
||||
config.detLimitSideLen,
|
||||
config.detLimitType,
|
||||
config.detMaxSideLimit,
|
||||
config.detImgMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun detect(src: Mat): DetectionResult {
|
||||
return detect {
|
||||
DetPreprocessor.preprocess(
|
||||
src,
|
||||
config.detLimitSideLen,
|
||||
config.detLimitType,
|
||||
config.detMaxSideLimit,
|
||||
config.detImgMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun detect(preprocessor: () -> DetPreprocessResult): DetectionResult {
|
||||
val preStart = System.currentTimeMillis()
|
||||
val preResult = preprocessor()
|
||||
val preprocessMs = System.currentTimeMillis() - preStart
|
||||
|
||||
val infStart = System.currentTimeMillis()
|
||||
val (outputData, outputShape) = ortManager.runDetection(preResult.tensorData, preResult.shape)
|
||||
val inferenceMs = System.currentTimeMillis() - infStart
|
||||
|
||||
val postStart = System.currentTimeMillis()
|
||||
val boxes = DBPostProcessor.process(
|
||||
pred = outputData,
|
||||
predShape = outputShape,
|
||||
thresh = config.detThresh,
|
||||
boxThresh = config.detBoxThresh,
|
||||
unclipRatio = config.detUnclipRatio,
|
||||
maxCandidates = config.detMaxCandidates,
|
||||
useDilation = config.detUseDilation,
|
||||
scoreMode = config.detScoreMode,
|
||||
boxType = config.detBoxType,
|
||||
originalH = preResult.originalH,
|
||||
originalW = preResult.originalW,
|
||||
)
|
||||
val postprocessMs = System.currentTimeMillis() - postStart
|
||||
|
||||
return DetectionResult(
|
||||
boxes = boxes,
|
||||
preprocessMs = preprocessMs,
|
||||
inferenceMs = inferenceMs,
|
||||
postprocessMs = postprocessMs,
|
||||
timeMs = preprocessMs + inferenceMs + postprocessMs,
|
||||
inputShape = listOf(1, 3, preResult.shape[2].toInt(), preResult.shape[3].toInt()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.engine
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import com.paddle.ocr.EngineConfig
|
||||
import com.paddle.ocr.PaddleOCRConfig
|
||||
import com.paddle.ocr.model.ModelConfig
|
||||
import com.paddle.ocr.model.OCRError
|
||||
import com.paddle.ocr.model.OCRResult
|
||||
import com.paddle.ocr.postprocess.BoxSorter
|
||||
import com.paddle.ocr.postprocess.QuadTextCrop
|
||||
import com.paddle.ocr.util.BitmapUtils
|
||||
|
||||
class OCREngine(
|
||||
context: Context,
|
||||
private val config: PaddleOCRConfig,
|
||||
engineConfig: EngineConfig,
|
||||
detModelAsset: String = "models/det/inference.onnx",
|
||||
recModelAsset: String = "models/rec/inference.onnx",
|
||||
recConfigAsset: String = "models/rec/inference.yml",
|
||||
) {
|
||||
private val ortManager = ORTSessionManager(context, engineConfig)
|
||||
private val detectionEngine: DetectionEngine
|
||||
private val recognitionEngine: RecognitionEngine
|
||||
val coldLoadTimeMs: Long get() = ortManager.coldLoadTimeMs
|
||||
|
||||
init {
|
||||
val configured = try {
|
||||
ortManager.loadModels(detModelAsset, recModelAsset)
|
||||
val recModelConfig = ModelConfig.parse(context, recConfigAsset)
|
||||
recModelConfig
|
||||
} catch (t: Throwable) {
|
||||
ortManager.release()
|
||||
throw t
|
||||
}
|
||||
detectionEngine = DetectionEngine(ortManager, config)
|
||||
recognitionEngine = RecognitionEngine(ortManager, configured.characterList)
|
||||
}
|
||||
|
||||
fun run(bitmap: Bitmap): OCREngineResult {
|
||||
val srcMat = BitmapUtils.bitmapToBGRMat(bitmap)
|
||||
return runWithOwnedMat(srcMat)
|
||||
}
|
||||
|
||||
fun run(imageBytes: ByteArray): OCREngineResult {
|
||||
val srcMat = BitmapUtils.imdecodeBGR(imageBytes)
|
||||
if (srcMat.empty()) {
|
||||
srcMat.release()
|
||||
throw OCRError.InvalidImage()
|
||||
}
|
||||
return runWithOwnedMat(srcMat)
|
||||
}
|
||||
|
||||
private fun runWithOwnedMat(srcMat: org.opencv.core.Mat): OCREngineResult {
|
||||
return try {
|
||||
run(srcMat)
|
||||
} finally {
|
||||
srcMat.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun run(srcMat: org.opencv.core.Mat): OCREngineResult {
|
||||
val totalStart = System.currentTimeMillis()
|
||||
val detResult = detectionEngine.detect(srcMat)
|
||||
val boxes = detResult.boxes
|
||||
|
||||
if (boxes.isEmpty()) {
|
||||
val elapsed = System.currentTimeMillis() - totalStart
|
||||
return OCREngineResult(
|
||||
results = emptyList(),
|
||||
detectionTimeMs = detResult.timeMs,
|
||||
recognitionTimeMs = 0,
|
||||
totalTimeMs = elapsed,
|
||||
lineCount = 0,
|
||||
detPreprocessMs = detResult.preprocessMs,
|
||||
detInferenceMs = detResult.inferenceMs,
|
||||
detPostprocessMs = detResult.postprocessMs,
|
||||
detInputShape = detResult.inputShape,
|
||||
coldLoadTimeMs = ortManager.coldLoadTimeMs,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Sort boxes
|
||||
val sortedBoxes = BoxSorter.sortInReadingOrder(boxes)
|
||||
|
||||
// 3. Crop and recognize text regions
|
||||
var totalRecPreMs = 0L
|
||||
var totalRecInfMs = 0L
|
||||
var totalRecPostMs = 0L
|
||||
var totalRecMs = 0L
|
||||
val allResults = mutableListOf<OCRResult>()
|
||||
val recInputShapes = mutableListOf<List<Int>>()
|
||||
val perLineRecMs = mutableListOf<Long>()
|
||||
val batchSize = config.recBatchSize.coerceAtLeast(1)
|
||||
|
||||
var i = 0
|
||||
while (i < sortedBoxes.size) {
|
||||
val batchCrops = mutableListOf<org.opencv.core.Mat>()
|
||||
val batchBoxIndices = mutableListOf<Int>()
|
||||
var next = i
|
||||
while (next < sortedBoxes.size && batchCrops.size < batchSize) {
|
||||
val crop = QuadTextCrop.crop(srcMat, sortedBoxes[next])
|
||||
if (crop.rows() > 0 && crop.cols() > 0) {
|
||||
batchCrops.add(crop)
|
||||
batchBoxIndices.add(next)
|
||||
} else {
|
||||
crop.release()
|
||||
}
|
||||
next++
|
||||
}
|
||||
|
||||
try {
|
||||
if (batchCrops.isNotEmpty()) {
|
||||
val batchResult = recognitionEngine.recognize(batchCrops)
|
||||
totalRecPreMs += batchResult.preprocessMs
|
||||
totalRecInfMs += batchResult.inferenceMs
|
||||
totalRecPostMs += batchResult.postprocessMs
|
||||
totalRecMs += batchResult.timeMs
|
||||
recInputShapes.add(batchResult.inputShape)
|
||||
if (batchSize == 1) {
|
||||
perLineRecMs.add(batchResult.timeMs)
|
||||
}
|
||||
|
||||
for (j in batchResult.texts.indices) {
|
||||
val boxIdx = batchBoxIndices[j]
|
||||
val (text, confidence) = batchResult.texts[j]
|
||||
if (confidence >= config.recScoreThresh) {
|
||||
allResults.add(
|
||||
OCRResult(
|
||||
box = sortedBoxes[boxIdx],
|
||||
text = text,
|
||||
confidence = confidence,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
batchCrops.forEach { it.release() }
|
||||
}
|
||||
i = next
|
||||
}
|
||||
|
||||
val totalElapsed = System.currentTimeMillis() - totalStart
|
||||
val pipelineOverhead = totalElapsed - detResult.timeMs - totalRecMs
|
||||
|
||||
return OCREngineResult(
|
||||
results = allResults,
|
||||
detectionTimeMs = detResult.timeMs,
|
||||
recognitionTimeMs = totalRecMs,
|
||||
totalTimeMs = totalElapsed,
|
||||
lineCount = allResults.size,
|
||||
detPreprocessMs = detResult.preprocessMs,
|
||||
detInferenceMs = detResult.inferenceMs,
|
||||
detPostprocessMs = detResult.postprocessMs,
|
||||
recPreprocessMs = totalRecPreMs,
|
||||
recInferenceMs = totalRecInfMs,
|
||||
recPostprocessMs = totalRecPostMs,
|
||||
pipelineOverheadMs = pipelineOverhead,
|
||||
coldLoadTimeMs = ortManager.coldLoadTimeMs,
|
||||
detInputShape = detResult.inputShape,
|
||||
recInputShapes = recInputShapes,
|
||||
perLineRecMs = perLineRecMs,
|
||||
)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
ortManager.release()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.engine
|
||||
|
||||
import com.paddle.ocr.model.OCRResult
|
||||
|
||||
data class OCREngineResult(
|
||||
val results: List<OCRResult>,
|
||||
val detectionTimeMs: Long,
|
||||
val recognitionTimeMs: Long,
|
||||
val totalTimeMs: Long,
|
||||
val lineCount: Int,
|
||||
// Detailed per-stage timing
|
||||
val detPreprocessMs: Long = 0,
|
||||
val detInferenceMs: Long = 0,
|
||||
val detPostprocessMs: Long = 0,
|
||||
val recPreprocessMs: Long = 0,
|
||||
val recInferenceMs: Long = 0,
|
||||
val recPostprocessMs: Long = 0,
|
||||
val pipelineOverheadMs: Long = 0,
|
||||
val coldLoadTimeMs: Long = 0,
|
||||
// Input tensor shapes
|
||||
val detInputShape: List<Int> = emptyList(),
|
||||
val recInputShapes: List<List<Int>> = emptyList(),
|
||||
// Per-line timing (only populated when recBatchSize == 1)
|
||||
val perLineRecMs: List<Long> = emptyList(),
|
||||
)
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.engine
|
||||
|
||||
import ai.onnxruntime.OnnxTensor
|
||||
import ai.onnxruntime.OrtEnvironment
|
||||
import ai.onnxruntime.OrtSession
|
||||
import android.content.Context
|
||||
import com.paddle.ocr.EngineConfig
|
||||
import com.paddle.ocr.model.OCRError
|
||||
import java.nio.FloatBuffer
|
||||
|
||||
class ORTSessionManager(
|
||||
private val context: Context,
|
||||
private val config: EngineConfig,
|
||||
) {
|
||||
private var env: OrtEnvironment? = null
|
||||
private var detSession: OrtSession? = null
|
||||
private var recSession: OrtSession? = null
|
||||
private var detInputName: String = "x"
|
||||
private var recInputName: String = "x"
|
||||
var coldLoadTimeMs: Long = 0
|
||||
private set
|
||||
|
||||
fun loadModels(detAssetPath: String, recAssetPath: String) {
|
||||
val loadStart = System.currentTimeMillis()
|
||||
env = OrtEnvironment.getEnvironment()
|
||||
val opts = OrtSession.SessionOptions().apply {
|
||||
setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT)
|
||||
setIntraOpNumThreads(config.numThreads)
|
||||
}
|
||||
try {
|
||||
val ortEnv = env ?: throw OCRError.ModelLoadFailed("OCR", Exception("Environment not initialized"))
|
||||
val detBytes = readModelAsset(detAssetPath)
|
||||
val recBytes = readModelAsset(recAssetPath)
|
||||
try {
|
||||
detSession = ortEnv.createSession(detBytes, opts)
|
||||
} catch (t: Throwable) {
|
||||
throw OCRError.ModelLoadFailed("detection", t)
|
||||
}
|
||||
try {
|
||||
recSession = ortEnv.createSession(recBytes, opts)
|
||||
} catch (t: Throwable) {
|
||||
detSession?.close()
|
||||
detSession = null
|
||||
throw OCRError.ModelLoadFailed("recognition", t)
|
||||
}
|
||||
|
||||
detInputName = try {
|
||||
detSession!!.inputNames.iterator().next()
|
||||
} catch (t: Throwable) {
|
||||
throw OCRError.ModelLoadFailed("detection", t)
|
||||
}
|
||||
recInputName = try {
|
||||
recSession!!.inputNames.iterator().next()
|
||||
} catch (t: Throwable) {
|
||||
throw OCRError.ModelLoadFailed("recognition", t)
|
||||
}
|
||||
coldLoadTimeMs = System.currentTimeMillis() - loadStart
|
||||
} finally {
|
||||
opts.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun runDetection(input: FloatArray, shape: LongArray): Pair<FloatArray, LongArray> {
|
||||
val session = detSession
|
||||
?: throw OCRError.ModelLoadFailed("detection", Exception("Session not initialized"))
|
||||
val ortEnv = env
|
||||
?: throw OCRError.ModelLoadFailed("detection", Exception("Environment not initialized"))
|
||||
return runSession(ortEnv, session, detInputName, input, shape, "detection")
|
||||
}
|
||||
|
||||
fun runRecognition(input: FloatArray, shape: LongArray): Pair<FloatArray, LongArray> {
|
||||
val session = recSession
|
||||
?: throw OCRError.ModelLoadFailed("recognition", Exception("Session not initialized"))
|
||||
val ortEnv = env
|
||||
?: throw OCRError.ModelLoadFailed("recognition", Exception("Environment not initialized"))
|
||||
return runSession(ortEnv, session, recInputName, input, shape, "recognition")
|
||||
}
|
||||
|
||||
fun release() {
|
||||
try {
|
||||
detSession?.close()
|
||||
} finally {
|
||||
detSession = null
|
||||
try {
|
||||
recSession?.close()
|
||||
} finally {
|
||||
recSession = null
|
||||
env = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readModelAsset(assetPath: String): ByteArray {
|
||||
return try {
|
||||
context.assets.open(assetPath).use { it.readBytes() }
|
||||
} catch (t: Throwable) {
|
||||
throw OCRError.ModelNotFound(assetPath, t)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runSession(
|
||||
ortEnv: OrtEnvironment,
|
||||
session: OrtSession,
|
||||
inputName: String,
|
||||
input: FloatArray,
|
||||
shape: LongArray,
|
||||
modelName: String,
|
||||
): Pair<FloatArray, LongArray> {
|
||||
val tensor = try {
|
||||
OnnxTensor.createTensor(ortEnv, FloatBuffer.wrap(input), shape)
|
||||
} catch (t: Throwable) {
|
||||
throw OCRError.InferenceFailed(modelName, t)
|
||||
}
|
||||
val result = try {
|
||||
try {
|
||||
session.run(mapOf(inputName to tensor))
|
||||
} catch (t: Throwable) {
|
||||
throw OCRError.InferenceFailed(modelName, t)
|
||||
}
|
||||
} finally {
|
||||
tensor.close()
|
||||
}
|
||||
|
||||
return try {
|
||||
try {
|
||||
val outputName = session.outputNames.iterator().next()
|
||||
val ortValue = result.get(outputName)
|
||||
.orElseThrow { Exception("No output tensor found") }
|
||||
val outputTensor = ortValue as? OnnxTensor
|
||||
?: throw Exception("Output is not an ONNX tensor")
|
||||
Pair(copyFloatBuffer(outputTensor.floatBuffer), outputTensor.info.shape)
|
||||
} catch (t: Throwable) {
|
||||
throw OCRError.InferenceFailed(modelName, t)
|
||||
}
|
||||
} finally {
|
||||
result.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyFloatBuffer(buffer: FloatBuffer): FloatArray {
|
||||
val duplicate = buffer.duplicate()
|
||||
duplicate.rewind()
|
||||
val output = FloatArray(duplicate.remaining())
|
||||
duplicate.get(output)
|
||||
return output
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.engine
|
||||
|
||||
import com.paddle.ocr.postprocess.CTCDecoder
|
||||
import com.paddle.ocr.preprocess.RecPreprocessor
|
||||
import org.opencv.core.Mat
|
||||
|
||||
class RecognitionEngine(
|
||||
private val ortManager: ORTSessionManager,
|
||||
private val characterList: List<String>,
|
||||
) {
|
||||
data class RecognitionResult(
|
||||
val texts: List<Pair<String, Float>>,
|
||||
val preprocessMs: Long,
|
||||
val inferenceMs: Long,
|
||||
val postprocessMs: Long,
|
||||
val timeMs: Long,
|
||||
val inputShape: List<Int>,
|
||||
)
|
||||
|
||||
fun recognize(crops: List<Mat>): RecognitionResult {
|
||||
// Preprocess
|
||||
val preStart = System.currentTimeMillis()
|
||||
val preResult = RecPreprocessor.preprocessBatch(crops)
|
||||
val preprocessMs = System.currentTimeMillis() - preStart
|
||||
|
||||
// Inference
|
||||
val infStart = System.currentTimeMillis()
|
||||
val (outputData, outputShape) = ortManager.runRecognition(preResult.tensorData, preResult.shape)
|
||||
val inferenceMs = System.currentTimeMillis() - infStart
|
||||
|
||||
// Postprocess (CTC decode)
|
||||
val postStart = System.currentTimeMillis()
|
||||
val decoded = CTCDecoder.decode(outputData, outputShape, characterList)
|
||||
val postprocessMs = System.currentTimeMillis() - postStart
|
||||
|
||||
val inputShape = preResult.shape.map { it.toInt() }
|
||||
val timeMs = preprocessMs + inferenceMs + postprocessMs
|
||||
return RecognitionResult(
|
||||
texts = decoded,
|
||||
preprocessMs = preprocessMs,
|
||||
inferenceMs = inferenceMs,
|
||||
postprocessMs = postprocessMs,
|
||||
timeMs = timeMs,
|
||||
inputShape = inputShape,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.model
|
||||
|
||||
import android.content.Context
|
||||
import com.paddle.ocr.util.YamlUtils
|
||||
|
||||
data class ModelConfig(
|
||||
val characterList: List<String>,
|
||||
) {
|
||||
companion object {
|
||||
fun parse(context: Context, assetPath: String): ModelConfig {
|
||||
val content = try {
|
||||
context.assets.open(assetPath).bufferedReader().use { it.readText() }
|
||||
} catch (t: Throwable) {
|
||||
throw OCRError.ConfigParseFailed(assetPath, t)
|
||||
}
|
||||
val characterDict = try {
|
||||
extractCharacterDict(content, assetPath)
|
||||
} catch (e: OCRError.ConfigParseFailed) {
|
||||
throw e
|
||||
} catch (t: Throwable) {
|
||||
throw OCRError.ConfigParseFailed(assetPath, t)
|
||||
}
|
||||
val charListWithSpace = characterDict.toMutableList().apply {
|
||||
if (lastOrNull() != " ") add(" ")
|
||||
}
|
||||
|
||||
return ModelConfig(characterList = charListWithSpace)
|
||||
}
|
||||
|
||||
private fun extractCharacterDict(content: String, assetPath: String): List<String> {
|
||||
val lines = content.replace("\r\n", "\n").replace('\r', '\n').lines()
|
||||
val postProcessLine = lines.indexOfFirst { it.trim() == "PostProcess:" }
|
||||
if (postProcessLine < 0) {
|
||||
throw OCRError.ConfigParseFailed("Missing PostProcess in $assetPath")
|
||||
}
|
||||
|
||||
val postProcessIndent = YamlUtils.leadingSpaces(lines[postProcessLine])
|
||||
val characterDictLine = findCharacterDictLine(lines, postProcessLine + 1, postProcessIndent)
|
||||
if (characterDictLine < 0) {
|
||||
throw OCRError.ConfigParseFailed("Missing character_dict in $assetPath")
|
||||
}
|
||||
|
||||
val characterDictIndent = YamlUtils.leadingSpaces(lines[characterDictLine])
|
||||
val characters = mutableListOf<String>()
|
||||
var lineIndex = characterDictLine + 1
|
||||
while (lineIndex < lines.size) {
|
||||
val line = lines[lineIndex]
|
||||
val indent = YamlUtils.leadingSpaces(line)
|
||||
val content = line.substring(indent)
|
||||
val keyLikeContent = content.trim()
|
||||
if (keyLikeContent.isEmpty() || keyLikeContent.startsWith("#")) {
|
||||
lineIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
if (!content.startsWith("-")) {
|
||||
if (indent <= characterDictIndent) break
|
||||
lineIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
characters.add(parseYamlListScalar(content.substring(1)))
|
||||
lineIndex++
|
||||
}
|
||||
|
||||
if (characters.isEmpty()) {
|
||||
throw OCRError.ConfigParseFailed("Empty character_dict in $assetPath")
|
||||
}
|
||||
return characters
|
||||
}
|
||||
|
||||
private fun findCharacterDictLine(
|
||||
lines: List<String>,
|
||||
startLine: Int,
|
||||
postProcessIndent: Int,
|
||||
): Int {
|
||||
var lineIndex = startLine
|
||||
while (lineIndex < lines.size) {
|
||||
val line = lines[lineIndex]
|
||||
val trimmed = line.trim()
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("#")) {
|
||||
lineIndex++
|
||||
continue
|
||||
}
|
||||
val indent = YamlUtils.leadingSpaces(line)
|
||||
if (indent <= postProcessIndent) break
|
||||
if (trimmed == "character_dict:") return lineIndex
|
||||
lineIndex++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun parseYamlListScalar(rawValue: String): String {
|
||||
val value = rawValue.dropWhile { it == ' ' }
|
||||
if (value.length >= 2 && value.first() == '\'' && value.last() == '\'') {
|
||||
return value.substring(1, value.length - 1).replace("''", "'")
|
||||
}
|
||||
if (value.length >= 2 && value.first() == '"' && value.last() == '"') {
|
||||
return value.substring(1, value.length - 1)
|
||||
.replace("\\\"", "\"")
|
||||
.replace("\\\\", "\\")
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\r", "\r")
|
||||
.replace("\\t", "\t")
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.model
|
||||
|
||||
import android.graphics.PointF
|
||||
|
||||
data class OCRBox(
|
||||
val points: List<PointF>,
|
||||
) {
|
||||
init {
|
||||
require(points.size == 4) { "OCRBox must have exactly 4 points, got ${points.size}" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.model
|
||||
|
||||
sealed class OCRError(message: String, cause: Throwable? = null) : Exception(message, cause) {
|
||||
class ModelNotFound(modelPath: String, cause: Throwable? = null) : OCRError("Model not found: $modelPath", cause)
|
||||
class ModelLoadFailed(modelName: String, cause: Throwable) : OCRError("Failed to load $modelName model", cause)
|
||||
class ConfigParseFailed(path: String, cause: Throwable? = null) : OCRError("Failed to parse config: $path", cause)
|
||||
class InvalidImage : OCRError("Input image is empty or invalid")
|
||||
class InferenceFailed(stage: String, cause: Throwable) : OCRError("Inference failed at stage '$stage'", cause)
|
||||
class DecodeError(message: String, cause: Throwable? = null) : OCRError(message, cause)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.model
|
||||
|
||||
data class OCRResult(
|
||||
val box: OCRBox,
|
||||
val text: String,
|
||||
val confidence: Float,
|
||||
val wordBoxes: List<OCRBox>? = null,
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.model
|
||||
|
||||
data class OCRRunResult(
|
||||
val results: List<OCRResult>,
|
||||
val detectionTimeMs: Long,
|
||||
val recognitionTimeMs: Long,
|
||||
val totalTimeMs: Long,
|
||||
val lineCount: Int,
|
||||
// Detailed per-stage timing
|
||||
val detPreprocessMs: Long = 0,
|
||||
val detInferenceMs: Long = 0,
|
||||
val detPostprocessMs: Long = 0,
|
||||
val recPreprocessMs: Long = 0,
|
||||
val recInferenceMs: Long = 0,
|
||||
val recPostprocessMs: Long = 0,
|
||||
val pipelineOverheadMs: Long = 0,
|
||||
val coldLoadTimeMs: Long = 0,
|
||||
// Input tensor shapes
|
||||
val detInputShape: List<Int> = emptyList(),
|
||||
val recInputShapes: List<List<Int>> = emptyList(),
|
||||
// Per-line timing (only populated when recBatchSize == 1)
|
||||
val perLineRecMs: List<Long> = emptyList(),
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.postprocess
|
||||
|
||||
import com.paddle.ocr.model.OCRBox
|
||||
|
||||
object BoxSorter {
|
||||
private const val ROW_THRESHOLD_Y = 10f
|
||||
|
||||
fun sortInReadingOrder(boxes: List<OCRBox>): List<OCRBox> {
|
||||
if (boxes.size <= 1) return boxes
|
||||
|
||||
val list = boxes.toMutableList()
|
||||
list.sortWith(compareBy({ it.points[0].y }, { it.points[0].x }))
|
||||
|
||||
// Align with PaddleX SortQuadBoxes: bubble left-to-right inside a 10px row band.
|
||||
for (i in 0 until list.size - 1) {
|
||||
var j = i
|
||||
while (j >= 0) {
|
||||
val next = list[j + 1]
|
||||
val curr = list[j]
|
||||
if (kotlin.math.abs(next.points[0].y - curr.points[0].y) < ROW_THRESHOLD_Y &&
|
||||
next.points[0].x < curr.points[0].x
|
||||
) {
|
||||
list[j] = next
|
||||
list[j + 1] = curr
|
||||
j--
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.postprocess
|
||||
|
||||
object CTCDecoder {
|
||||
private const val BLANK_IDX = 0
|
||||
|
||||
fun decode(output: FloatArray, shape: LongArray, characterList: List<String>): List<Pair<String, Float>> {
|
||||
val batchSize = shape[0].toInt()
|
||||
val timeSteps = shape[1].toInt()
|
||||
val numClasses = shape[2].toInt()
|
||||
|
||||
val results = mutableListOf<Pair<String, Float>>()
|
||||
for (b in 0 until batchSize) {
|
||||
val baseOffset = b * timeSteps * numClasses
|
||||
|
||||
val indices = IntArray(timeSteps)
|
||||
val probs = FloatArray(timeSteps)
|
||||
for (t in 0 until timeSteps) {
|
||||
val offset = baseOffset + t * numClasses
|
||||
var maxIdx = 0
|
||||
var maxVal = output[offset]
|
||||
for (c in 1 until numClasses) {
|
||||
val v = output[offset + c]
|
||||
if (v > maxVal) {
|
||||
maxVal = v
|
||||
maxIdx = c
|
||||
}
|
||||
}
|
||||
indices[t] = maxIdx
|
||||
probs[t] = maxVal
|
||||
}
|
||||
|
||||
val keptProbs = mutableListOf<Float>()
|
||||
val sb = StringBuilder()
|
||||
var prevIdx = -1
|
||||
for (t in 0 until timeSteps) {
|
||||
val idx = indices[t]
|
||||
if (idx != BLANK_IDX && idx != prevIdx) {
|
||||
val charIdx = idx - 1
|
||||
if (charIdx >= 0 && charIdx < characterList.size) {
|
||||
sb.append(characterList[charIdx])
|
||||
keptProbs.add(probs[t])
|
||||
}
|
||||
}
|
||||
prevIdx = idx
|
||||
}
|
||||
|
||||
val confidence = if (keptProbs.isNotEmpty()) keptProbs.average().toFloat() else 0f
|
||||
results.add(Pair(sb.toString(), confidence))
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.postprocess
|
||||
|
||||
import android.graphics.PointF
|
||||
import com.paddle.ocr.model.OCRBox
|
||||
import com.paddle.ocr.util.MathUtils
|
||||
import org.opencv.core.Core
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
import org.opencv.core.MatOfPoint
|
||||
import org.opencv.core.MatOfPoint2f
|
||||
import org.opencv.core.Point
|
||||
import org.opencv.core.Scalar
|
||||
import org.opencv.imgproc.Imgproc
|
||||
|
||||
object DBPostProcessor {
|
||||
private const val MIN_SIZE_BEFORE_UNCLIP = 3f
|
||||
private const val MIN_SIZE_AFTER_UNCLIP = 5f
|
||||
|
||||
fun process(
|
||||
pred: FloatArray,
|
||||
predShape: LongArray,
|
||||
thresh: Float,
|
||||
boxThresh: Float,
|
||||
unclipRatio: Float,
|
||||
maxCandidates: Int,
|
||||
useDilation: Boolean,
|
||||
scoreMode: String,
|
||||
boxType: String,
|
||||
originalH: Int,
|
||||
originalW: Int,
|
||||
): List<OCRBox> {
|
||||
require(boxType == "quad") { "Only DBPostProcess box_type=quad is supported" }
|
||||
|
||||
val pH = predShape[2].toInt()
|
||||
val pW = predShape[3].toInt()
|
||||
val scaleX = originalW.toDouble() / pW
|
||||
val scaleY = originalH.toDouble() / pH
|
||||
val normalizedScoreMode = scoreMode.lowercase()
|
||||
|
||||
val rawProb = Mat(pH, pW, CvType.CV_32FC1)
|
||||
val mask = Mat(pH, pW, CvType.CV_8UC1)
|
||||
val contours = mutableListOf<MatOfPoint>()
|
||||
val hierarchy = Mat()
|
||||
var contourMask = mask
|
||||
|
||||
return try {
|
||||
rawProb.put(0, 0, pred)
|
||||
val threshMat = Mat()
|
||||
try {
|
||||
Imgproc.threshold(rawProb, threshMat, thresh.toDouble(), 255.0, Imgproc.THRESH_BINARY)
|
||||
threshMat.convertTo(mask, CvType.CV_8UC1)
|
||||
} finally {
|
||||
threshMat.release()
|
||||
}
|
||||
|
||||
contourMask = if (useDilation) {
|
||||
val kernel = Mat.ones(2, 2, CvType.CV_8UC1)
|
||||
try {
|
||||
Mat().also { Imgproc.dilate(mask, it, kernel) }
|
||||
} finally {
|
||||
kernel.release()
|
||||
}
|
||||
} else {
|
||||
mask
|
||||
}
|
||||
|
||||
Imgproc.findContours(contourMask, contours, hierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE)
|
||||
val boxes = mutableListOf<OCRBox>()
|
||||
val numContours = minOf(contours.size, maxCandidates)
|
||||
for (index in 0 until numContours) {
|
||||
val contour = contours[index]
|
||||
val contour2f = MatOfPoint2f(*contour.toArray())
|
||||
val rect = try {
|
||||
Imgproc.minAreaRect(contour2f)
|
||||
} finally {
|
||||
contour2f.release()
|
||||
}
|
||||
val sside = minOf(rect.size.width, rect.size.height)
|
||||
if (sside < MIN_SIZE_BEFORE_UNCLIP) continue
|
||||
|
||||
val rectPoints = Array(4) { Point() }
|
||||
rect.points(rectPoints)
|
||||
val orderedPoints = QuadGeometry.orderMinAreaRectPoints(rectPoints)
|
||||
val score = if (normalizedScoreMode == "slow") {
|
||||
computeBoxScore(rawProb, contour.toList())
|
||||
} else {
|
||||
computeBoxScore(rawProb, orderedPoints)
|
||||
}
|
||||
if (score < boxThresh) continue
|
||||
|
||||
val expandedPts = PolygonUnclip.unclip(orderedPoints, unclipRatio)
|
||||
val expandedRect = MatOfPoint2f().let { expanded2f ->
|
||||
try {
|
||||
expanded2f.fromList(expandedPts)
|
||||
Imgproc.minAreaRect(expanded2f)
|
||||
} finally {
|
||||
expanded2f.release()
|
||||
}
|
||||
}
|
||||
val sside2 = minOf(expandedRect.size.width, expandedRect.size.height)
|
||||
if (sside2 < MIN_SIZE_AFTER_UNCLIP) continue
|
||||
|
||||
val expandedBox = Array(4) { Point() }
|
||||
expandedRect.points(expandedBox)
|
||||
val ePts = QuadGeometry.orderMinAreaRectPoints(expandedBox)
|
||||
val scaled = listOf(
|
||||
scalePoint(ePts[0], scaleX, scaleY, originalW, originalH),
|
||||
scalePoint(ePts[1], scaleX, scaleY, originalW, originalH),
|
||||
scalePoint(ePts[2], scaleX, scaleY, originalW, originalH),
|
||||
scalePoint(ePts[3], scaleX, scaleY, originalW, originalH),
|
||||
)
|
||||
|
||||
val boxW = kotlin.math.hypot(
|
||||
scaled[1].x - scaled[0].x,
|
||||
scaled[1].y - scaled[0].y,
|
||||
)
|
||||
val boxH = kotlin.math.hypot(
|
||||
scaled[3].x - scaled[0].x,
|
||||
scaled[3].y - scaled[0].y,
|
||||
)
|
||||
if (boxW <= 3 || boxH <= 3) continue
|
||||
|
||||
boxes.add(OCRBox(points = scaled))
|
||||
}
|
||||
boxes
|
||||
} finally {
|
||||
hierarchy.release()
|
||||
if (contourMask !== mask) {
|
||||
contourMask.release()
|
||||
}
|
||||
contours.forEach { it.release() }
|
||||
mask.release()
|
||||
rawProb.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scalePoint(
|
||||
point: Point,
|
||||
scaleX: Double,
|
||||
scaleY: Double,
|
||||
originalW: Int,
|
||||
originalH: Int,
|
||||
): PointF {
|
||||
return PointF(
|
||||
MathUtils.roundHalfToEven(point.x * scaleX).coerceIn(0, originalW).toFloat(),
|
||||
MathUtils.roundHalfToEven(point.y * scaleY).coerceIn(0, originalH).toFloat(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun computeBoxScore(probMap: Mat, points: List<Point>): Float {
|
||||
if (points.isEmpty()) return 0f
|
||||
|
||||
val h = probMap.rows()
|
||||
val w = probMap.cols()
|
||||
val xmin = points.minOf { kotlin.math.floor(it.x).toInt() }.coerceIn(0, w - 1)
|
||||
val xmax = points.maxOf { kotlin.math.ceil(it.x).toInt() }.coerceIn(0, w - 1)
|
||||
val ymin = points.minOf { kotlin.math.floor(it.y).toInt() }.coerceIn(0, h - 1)
|
||||
val ymax = points.maxOf { kotlin.math.ceil(it.y).toInt() }.coerceIn(0, h - 1)
|
||||
|
||||
val mask = Mat(ymax - ymin + 1, xmax - xmin + 1, CvType.CV_8UC1, Scalar(0.0))
|
||||
val pts = MatOfPoint().apply {
|
||||
fromList(
|
||||
points.map { Point((it.x - xmin).toInt().toDouble(), (it.y - ymin).toInt().toDouble()) }
|
||||
)
|
||||
}
|
||||
Imgproc.fillPoly(mask, mutableListOf(pts), Scalar(1.0))
|
||||
|
||||
val roi = probMap.submat(ymin, ymax + 1, xmin, xmax + 1)
|
||||
val mean = Core.mean(roi, mask)
|
||||
roi.release()
|
||||
mask.release()
|
||||
pts.release()
|
||||
return mean.`val`[0].toFloat()
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.postprocess
|
||||
|
||||
import org.opencv.core.Point
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.acos
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.sin
|
||||
|
||||
internal object PolygonUnclip {
|
||||
private const val EPS = 1e-6
|
||||
private const val ARC_TOLERANCE = 0.25
|
||||
|
||||
fun unclip(points: List<Point>, unclipRatio: Float): List<Point> {
|
||||
if (points.size < 3) return points
|
||||
|
||||
val signedArea = signedArea(points)
|
||||
val area = abs(signedArea)
|
||||
val perimeter = perimeter(points)
|
||||
if (!area.isFinite() || !perimeter.isFinite() || area <= EPS || perimeter <= EPS) {
|
||||
return points
|
||||
}
|
||||
|
||||
val distance = area * unclipRatio / perimeter
|
||||
if (!distance.isFinite() || distance <= EPS) return points
|
||||
|
||||
val clockwiseInImageCoords = signedArea > 0.0
|
||||
val normals = mutableListOf<Point>()
|
||||
for (i in points.indices) {
|
||||
val start = points[i]
|
||||
val end = points[(i + 1) % points.size]
|
||||
val dx = end.x - start.x
|
||||
val dy = end.y - start.y
|
||||
val len = hypot(dx, dy)
|
||||
if (!len.isFinite() || len <= EPS) return points
|
||||
|
||||
val normal = if (clockwiseInImageCoords) {
|
||||
Point(dy / len, -dx / len)
|
||||
} else {
|
||||
Point(-dy / len, dx / len)
|
||||
}
|
||||
normals.add(normal)
|
||||
}
|
||||
|
||||
val expanded = mutableListOf<Point>()
|
||||
for (i in points.indices) {
|
||||
appendRoundJoin(
|
||||
output = expanded,
|
||||
center = points[i],
|
||||
fromNormal = normals[(i - 1 + normals.size) % normals.size],
|
||||
toNormal = normals[i],
|
||||
distance = distance,
|
||||
clockwiseInImageCoords = clockwiseInImageCoords,
|
||||
)
|
||||
}
|
||||
return if (expanded.size >= 3) expanded else points
|
||||
}
|
||||
|
||||
private fun appendRoundJoin(
|
||||
output: MutableList<Point>,
|
||||
center: Point,
|
||||
fromNormal: Point,
|
||||
toNormal: Point,
|
||||
distance: Double,
|
||||
clockwiseInImageCoords: Boolean,
|
||||
) {
|
||||
var startAngle = atan2(fromNormal.y, fromNormal.x)
|
||||
var endAngle = atan2(toNormal.y, toNormal.x)
|
||||
if (clockwiseInImageCoords) {
|
||||
while (endAngle < startAngle) endAngle += 2.0 * PI
|
||||
} else {
|
||||
while (endAngle > startAngle) endAngle -= 2.0 * PI
|
||||
}
|
||||
|
||||
val sweep = endAngle - startAngle
|
||||
val steps = ceil(abs(sweep) / arcStepAngle(distance)).toInt().coerceAtLeast(1)
|
||||
for (step in 0..steps) {
|
||||
val angle = startAngle + sweep * step.toDouble() / steps.toDouble()
|
||||
appendPoint(
|
||||
output,
|
||||
Point(
|
||||
center.x + cos(angle) * distance,
|
||||
center.y + sin(angle) * distance,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun arcStepAngle(distance: Double): Double {
|
||||
val ratio = (1.0 - ARC_TOLERANCE / distance).coerceIn(-1.0, 1.0)
|
||||
val step = 2.0 * acos(ratio)
|
||||
return if (step.isFinite() && step > EPS) step else PI / 8.0
|
||||
}
|
||||
|
||||
private fun appendPoint(output: MutableList<Point>, point: Point) {
|
||||
val last = output.lastOrNull()
|
||||
if (last == null || hypot(point.x - last.x, point.y - last.y) > EPS) {
|
||||
output.add(point)
|
||||
}
|
||||
}
|
||||
|
||||
private fun signedArea(points: List<Point>): Double {
|
||||
var sum = 0.0
|
||||
for (i in points.indices) {
|
||||
val p1 = points[i]
|
||||
val p2 = points[(i + 1) % points.size]
|
||||
sum += p1.x * p2.y - p2.x * p1.y
|
||||
}
|
||||
return sum / 2.0
|
||||
}
|
||||
|
||||
private fun perimeter(points: List<Point>): Double {
|
||||
var length = 0.0
|
||||
for (i in points.indices) {
|
||||
val p1 = points[i]
|
||||
val p2 = points[(i + 1) % points.size]
|
||||
length += hypot(p2.x - p1.x, p2.y - p1.y)
|
||||
}
|
||||
return length
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.postprocess
|
||||
|
||||
import org.opencv.core.Point
|
||||
|
||||
internal object QuadGeometry {
|
||||
fun orderMinAreaRectPoints(points: Array<Point>): List<Point> {
|
||||
val sorted = points.sortedBy { it.x }
|
||||
val topLeft: Point
|
||||
val bottomLeft: Point
|
||||
if (sorted[1].y > sorted[0].y) {
|
||||
topLeft = sorted[0]
|
||||
bottomLeft = sorted[1]
|
||||
} else {
|
||||
topLeft = sorted[1]
|
||||
bottomLeft = sorted[0]
|
||||
}
|
||||
|
||||
val topRight: Point
|
||||
val bottomRight: Point
|
||||
if (sorted[3].y > sorted[2].y) {
|
||||
topRight = sorted[2]
|
||||
bottomRight = sorted[3]
|
||||
} else {
|
||||
topRight = sorted[3]
|
||||
bottomRight = sorted[2]
|
||||
}
|
||||
return listOf(topLeft, topRight, bottomRight, bottomLeft)
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.postprocess
|
||||
|
||||
import com.paddle.ocr.model.OCRBox
|
||||
import org.opencv.core.Core
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
import org.opencv.core.MatOfPoint2f
|
||||
import org.opencv.core.Point
|
||||
import org.opencv.core.Size
|
||||
import org.opencv.imgproc.Imgproc
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.max
|
||||
|
||||
object QuadTextCrop {
|
||||
private const val VERTICAL_CROP_RATIO = 1.5
|
||||
|
||||
fun crop(src: Mat, box: OCRBox): Mat {
|
||||
// Align with PaddleX CropByPolys.get_minarea_rect_crop: recompute minAreaRect
|
||||
// from the detected quad before perspective transform.
|
||||
val rectPoints = box.points.map { Point(it.x.toDouble(), it.y.toDouble()) }
|
||||
val rectInput = MatOfPoint2f()
|
||||
rectInput.fromList(rectPoints)
|
||||
val boundingBox = Imgproc.minAreaRect(rectInput)
|
||||
rectInput.release()
|
||||
|
||||
val boxPoints = Array(4) { Point() }
|
||||
boundingBox.points(boxPoints)
|
||||
val ordered = QuadGeometry.orderMinAreaRectPoints(boxPoints)
|
||||
|
||||
val widthTop = hypot(ordered[0].x - ordered[1].x, ordered[0].y - ordered[1].y)
|
||||
val widthBottom = hypot(ordered[2].x - ordered[3].x, ordered[2].y - ordered[3].y)
|
||||
val heightLeft = hypot(ordered[0].x - ordered[3].x, ordered[0].y - ordered[3].y)
|
||||
val heightRight = hypot(ordered[1].x - ordered[2].x, ordered[1].y - ordered[2].y)
|
||||
|
||||
val dstW = max(widthTop, widthBottom).toInt().coerceAtLeast(1)
|
||||
val dstH = max(heightLeft, heightRight).toInt().coerceAtLeast(1)
|
||||
|
||||
val srcPts = MatOfPoint2f()
|
||||
srcPts.fromList(ordered)
|
||||
val dstPts = MatOfPoint2f()
|
||||
dstPts.fromList(
|
||||
listOf(
|
||||
Point(0.0, 0.0),
|
||||
Point(dstW.toDouble(), 0.0),
|
||||
Point(dstW.toDouble(), dstH.toDouble()),
|
||||
Point(0.0, dstH.toDouble()),
|
||||
)
|
||||
)
|
||||
val m = Imgproc.getPerspectiveTransform(srcPts, dstPts)
|
||||
srcPts.release()
|
||||
dstPts.release()
|
||||
|
||||
val dst = Mat(dstH, dstW, CvType.CV_8UC3)
|
||||
Imgproc.warpPerspective(
|
||||
src,
|
||||
dst,
|
||||
m,
|
||||
Size(dstW.toDouble(), dstH.toDouble()),
|
||||
Imgproc.INTER_CUBIC,
|
||||
Core.BORDER_REPLICATE,
|
||||
)
|
||||
m.release()
|
||||
|
||||
if (dst.rows().toDouble() / dst.cols() >= VERTICAL_CROP_RATIO) {
|
||||
val rotated = Mat()
|
||||
Core.rotate(dst, rotated, Core.ROTATE_90_COUNTERCLOCKWISE)
|
||||
dst.release()
|
||||
return rotated
|
||||
}
|
||||
return dst
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.preprocess
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.paddle.ocr.util.BitmapUtils
|
||||
import com.paddle.ocr.util.ImageUtils
|
||||
import org.opencv.core.Core
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
import org.opencv.core.Scalar
|
||||
import org.opencv.imgproc.Imgproc
|
||||
|
||||
data class DetPreprocessResult(
|
||||
val tensorData: FloatArray,
|
||||
val shape: LongArray,
|
||||
val originalH: Int,
|
||||
val originalW: Int,
|
||||
)
|
||||
|
||||
object DetPreprocessor {
|
||||
private val mean = doubleArrayOf(0.485, 0.456, 0.406)
|
||||
private val std = doubleArrayOf(0.229, 0.224, 0.225)
|
||||
private const val scale = 1.0 / 255.0
|
||||
|
||||
fun preprocess(
|
||||
bitmap: Bitmap,
|
||||
limitSideLen: Int,
|
||||
limitType: String,
|
||||
maxSideLimit: Int,
|
||||
imgMode: String,
|
||||
): DetPreprocessResult {
|
||||
val src = BitmapUtils.bitmapToBGRMat(bitmap)
|
||||
return try {
|
||||
preprocess(src, limitSideLen, limitType, maxSideLimit, imgMode)
|
||||
} finally {
|
||||
src.release()
|
||||
}
|
||||
}
|
||||
|
||||
fun preprocess(
|
||||
src: Mat,
|
||||
limitSideLen: Int,
|
||||
limitType: String,
|
||||
maxSideLimit: Int,
|
||||
imgMode: String,
|
||||
): DetPreprocessResult {
|
||||
val originalH = src.rows()
|
||||
val originalW = src.cols()
|
||||
val input = if (imgMode.uppercase() == "RGB") {
|
||||
Mat().also { Imgproc.cvtColor(src, it, Imgproc.COLOR_BGR2RGB) }
|
||||
} else {
|
||||
src
|
||||
}
|
||||
|
||||
val resized = ImageUtils.resizeToMultipleOf32(input, limitSideLen, limitType, maxSideLimit)
|
||||
if (input !== src) input.release()
|
||||
|
||||
val h = resized.rows()
|
||||
val w = resized.cols()
|
||||
val floatMat = Mat(h, w, CvType.CV_32FC3)
|
||||
resized.convertTo(floatMat, CvType.CV_32F)
|
||||
|
||||
val channels = mutableListOf<Mat>()
|
||||
Core.split(floatMat, channels)
|
||||
for (c in 0..2) {
|
||||
Core.multiply(channels[c], Scalar(scale), channels[c])
|
||||
Core.subtract(channels[c], Scalar(mean[c]), channels[c])
|
||||
Core.divide(channels[c], Scalar(std[c]), channels[c])
|
||||
}
|
||||
resized.release()
|
||||
floatMat.release()
|
||||
|
||||
val channelSize = h * w
|
||||
val tensorData = FloatArray(3 * channelSize)
|
||||
for (c in 0..2) {
|
||||
val chan = channels[c]
|
||||
val buf = FloatArray(channelSize)
|
||||
chan.get(0, 0, buf)
|
||||
System.arraycopy(buf, 0, tensorData, c * channelSize, channelSize)
|
||||
chan.release()
|
||||
}
|
||||
|
||||
return DetPreprocessResult(
|
||||
tensorData = tensorData,
|
||||
shape = longArrayOf(1, 3, h.toLong(), w.toLong()),
|
||||
originalH = originalH,
|
||||
originalW = originalW,
|
||||
)
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.preprocess
|
||||
|
||||
import org.opencv.core.Core
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
import org.opencv.core.Size
|
||||
import org.opencv.imgproc.Imgproc
|
||||
import kotlin.math.ceil
|
||||
|
||||
data class RecPreprocessResult(
|
||||
val tensorData: FloatArray,
|
||||
val shape: LongArray,
|
||||
)
|
||||
|
||||
object RecPreprocessor {
|
||||
private const val FIXED_HEIGHT = 48
|
||||
private const val MAX_IMG_W = 3200
|
||||
|
||||
fun preprocessBatch(crops: List<Mat>): RecPreprocessResult {
|
||||
// Convert BGR to RGB and resize to fixed height while preserving aspect ratio
|
||||
val resizedMats = mutableListOf<Mat>()
|
||||
for (crop in crops) {
|
||||
// Convert BGR to RGB (model expects RGB input)
|
||||
val rgb = Mat()
|
||||
Imgproc.cvtColor(crop, rgb, Imgproc.COLOR_BGR2RGB)
|
||||
val h = rgb.rows()
|
||||
val w = rgb.cols()
|
||||
val aspectRatio = if (h > 0) w.toDouble() / h else 1.0
|
||||
val newW = ceil(FIXED_HEIGHT * aspectRatio).toInt().coerceAtMost(MAX_IMG_W)
|
||||
val dst = Mat()
|
||||
Imgproc.resize(rgb, dst, Size(newW.toDouble(), FIXED_HEIGHT.toDouble()), 0.0, 0.0, Imgproc.INTER_LINEAR)
|
||||
rgb.release()
|
||||
resizedMats.add(dst)
|
||||
}
|
||||
|
||||
// Convert to float and normalize: (x / 255 - 0.5) / 0.5 = x / 127.5 - 1
|
||||
val floatMats = mutableListOf<Mat>()
|
||||
for (mat in resizedMats) {
|
||||
val floatMat = Mat(mat.rows(), mat.cols(), CvType.CV_32FC3)
|
||||
mat.convertTo(floatMat, CvType.CV_32F)
|
||||
// Use Scalar with all 3 channels set — single-value Scalar only sets val[0]!
|
||||
Core.divide(floatMat, org.opencv.core.Scalar(127.5, 127.5, 127.5), floatMat)
|
||||
Core.subtract(floatMat, org.opencv.core.Scalar(1.0, 1.0, 1.0), floatMat)
|
||||
|
||||
floatMats.add(floatMat)
|
||||
mat.release() // Release resized mat
|
||||
}
|
||||
resizedMats.clear()
|
||||
|
||||
val maxW = floatMats.maxOf { it.cols() }
|
||||
val n = floatMats.size
|
||||
|
||||
// Pad to max width
|
||||
val paddedMats = mutableListOf<Mat>()
|
||||
for (mat in floatMats) {
|
||||
if (mat.cols() == maxW) {
|
||||
paddedMats.add(mat)
|
||||
} else {
|
||||
val padded = Mat(FIXED_HEIGHT, maxW, CvType.CV_32FC3, org.opencv.core.Scalar(0.0))
|
||||
val roi = padded.submat(0, FIXED_HEIGHT, 0, mat.cols())
|
||||
mat.copyTo(roi)
|
||||
roi.release()
|
||||
mat.release()
|
||||
paddedMats.add(padded)
|
||||
}
|
||||
}
|
||||
floatMats.clear()
|
||||
|
||||
// Build tensor data
|
||||
val channelSize = FIXED_HEIGHT * maxW
|
||||
val tensorData = FloatArray(n * 3 * channelSize)
|
||||
for (b in 0 until n) {
|
||||
val mat = paddedMats[b]
|
||||
val channels = mutableListOf<Mat>()
|
||||
Core.split(mat, channels)
|
||||
for (c in 0..2) {
|
||||
val buf = FloatArray(channelSize)
|
||||
channels[c].get(0, 0, buf)
|
||||
System.arraycopy(buf, 0, tensorData, (b * 3 + c) * channelSize, channelSize)
|
||||
channels[c].release()
|
||||
}
|
||||
mat.release()
|
||||
}
|
||||
paddedMats.clear()
|
||||
|
||||
return RecPreprocessResult(
|
||||
tensorData = tensorData,
|
||||
shape = longArrayOf(n.toLong(), 3, FIXED_HEIGHT.toLong(), maxW.toLong()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import org.opencv.android.Utils
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
import org.opencv.core.MatOfByte
|
||||
import org.opencv.imgcodecs.Imgcodecs
|
||||
import org.opencv.imgproc.Imgproc
|
||||
|
||||
object BitmapUtils {
|
||||
|
||||
fun imdecodeBGR(imageBytes: ByteArray): Mat {
|
||||
val encoded = MatOfByte(*imageBytes)
|
||||
return try {
|
||||
Imgcodecs.imdecode(encoded, Imgcodecs.IMREAD_COLOR)
|
||||
} finally {
|
||||
encoded.release()
|
||||
}
|
||||
}
|
||||
|
||||
fun bitmapToBGRMat(bitmap: Bitmap): Mat {
|
||||
return bitmapToMat(bitmap, Imgproc.COLOR_RGBA2BGR)
|
||||
}
|
||||
|
||||
fun bitmapToRGBMat(bitmap: Bitmap): Mat {
|
||||
return bitmapToMat(bitmap, Imgproc.COLOR_RGBA2RGB)
|
||||
}
|
||||
|
||||
fun bgrMatToBitmap(mat: Mat): Bitmap {
|
||||
val rgba = Mat()
|
||||
return try {
|
||||
Imgproc.cvtColor(mat, rgba, Imgproc.COLOR_BGR2RGBA)
|
||||
Bitmap.createBitmap(rgba.cols(), rgba.rows(), Bitmap.Config.ARGB_8888).also { bitmap ->
|
||||
Utils.matToBitmap(rgba, bitmap)
|
||||
}
|
||||
} finally {
|
||||
rgba.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bitmapToMat(bitmap: Bitmap, colorConversionCode: Int): Mat {
|
||||
val bmp = bitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
val rgba = Mat(bmp.height, bmp.width, CvType.CV_8UC4)
|
||||
val dst = Mat()
|
||||
return try {
|
||||
Utils.bitmapToMat(bmp, rgba)
|
||||
Imgproc.cvtColor(rgba, dst, colorConversionCode)
|
||||
dst
|
||||
} catch (t: Throwable) {
|
||||
dst.release()
|
||||
throw t
|
||||
} finally {
|
||||
bmp.recycle()
|
||||
rgba.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.util
|
||||
|
||||
import org.opencv.core.Mat
|
||||
import org.opencv.core.Size
|
||||
import org.opencv.imgproc.Imgproc
|
||||
|
||||
object ImageUtils {
|
||||
|
||||
fun resizeToMultipleOf32(
|
||||
src: Mat,
|
||||
limitSideLen: Int,
|
||||
limitType: String,
|
||||
maxSideLimit: Int,
|
||||
): Mat {
|
||||
val h = src.rows()
|
||||
val w = src.cols()
|
||||
var ratio = when (limitType.lowercase()) {
|
||||
"max" -> if (maxOf(h, w) > limitSideLen) limitSideLen.toDouble() / maxOf(h, w) else 1.0
|
||||
"min" -> if (minOf(h, w) < limitSideLen) limitSideLen.toDouble() / minOf(h, w) else 1.0
|
||||
"resize_long" -> limitSideLen.toDouble() / maxOf(h, w)
|
||||
else -> throw IllegalArgumentException("Unsupported det limit type: $limitType")
|
||||
}
|
||||
|
||||
var newH = (h * ratio).toInt()
|
||||
var newW = (w * ratio).toInt()
|
||||
if (maxOf(newH, newW) > maxSideLimit) {
|
||||
ratio = maxSideLimit.toDouble() / maxOf(newH, newW)
|
||||
newH = (newH * ratio).toInt()
|
||||
newW = (newW * ratio).toInt()
|
||||
}
|
||||
|
||||
newH = maxOf(MathUtils.roundHalfToEven(newH / 32.0) * 32, 32)
|
||||
newW = maxOf(MathUtils.roundHalfToEven(newW / 32.0) * 32, 32)
|
||||
val dst = Mat()
|
||||
Imgproc.resize(src, dst, Size(newW.toDouble(), newH.toDouble()), 0.0, 0.0, Imgproc.INTER_LINEAR)
|
||||
return dst
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.util
|
||||
|
||||
import kotlin.math.floor
|
||||
|
||||
object MathUtils {
|
||||
|
||||
fun roundHalfToEven(value: Double): Int {
|
||||
val floored = floor(value)
|
||||
val diff = value - floored
|
||||
return when {
|
||||
diff < 0.5 -> floored.toInt()
|
||||
diff > 0.5 -> floored.toInt() + 1
|
||||
floored.toInt() % 2 == 0 -> floored.toInt()
|
||||
else -> floored.toInt() + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
|
||||
object OpenCVUtils {
|
||||
|
||||
private var initialized = false
|
||||
|
||||
fun init(context: Context): Boolean {
|
||||
if (initialized) return true
|
||||
try {
|
||||
System.loadLibrary("opencv_java4")
|
||||
initialized = true
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
Log.e("OpenCVUtils", "Failed to initialize OpenCV: ${e.message}")
|
||||
}
|
||||
return initialized
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.paddle.ocr.util
|
||||
|
||||
object YamlUtils {
|
||||
|
||||
fun leadingSpaces(line: String): Int {
|
||||
return line.indexOfFirst { it != ' ' }.let { if (it < 0) line.length else it }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user