6.7.0 - Alpha16 - 打包应用支持自带离线 Paddle OCR 引擎, 避免依赖插件安装
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
package com.baidu.paddle.lite.ocr;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import org.opencv.android.OpenCVLoader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* @author PaddleOCR
|
||||
* Modified by TonyJiangWJ
|
||||
* @since 2023-08-06
|
||||
*/
|
||||
public class OCRPredictorNative {
|
||||
|
||||
private static final AtomicBoolean isSOLoaded = new AtomicBoolean();
|
||||
private static final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public static void loadLibrary() throws RuntimeException {
|
||||
if (!isSOLoaded.get() && isSOLoaded.compareAndSet(false, true)) {
|
||||
try {
|
||||
// 可能和 AJ 中的 OpenCV 冲突, 直接初始化一遍
|
||||
OpenCVLoader.initDebug();
|
||||
System.loadLibrary("Native");
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(
|
||||
"Load libNative.so failed, please check it exists in apk file.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long nativePointer;
|
||||
|
||||
public OCRPredictorNative(Config config) {
|
||||
lock.lock();
|
||||
try {
|
||||
loadLibrary();
|
||||
nativePointer = init(config.detModelFilename, config.recModelFilename, config.clsModelFilename, config.useOpenCL,
|
||||
config.cpuThreadNum, config.cpuPower);
|
||||
Log.i("OCRPredictorNative", "load success " + nativePointer);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<OcrResultModel> runImage(Bitmap originalImage, int max_size_len, int run_det, int run_cls, int run_rec) {
|
||||
lock.lock();
|
||||
try {
|
||||
Log.i("OCRPredictorNative", "begin to run image");
|
||||
float[] rawResults = forward(nativePointer, originalImage, max_size_len, run_det, run_cls, run_rec);
|
||||
return postprocess(rawResults);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
public int useOpenCL;
|
||||
public int cpuThreadNum;
|
||||
public String cpuPower;
|
||||
public String detModelFilename;
|
||||
public String recModelFilename;
|
||||
public String clsModelFilename;
|
||||
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (nativePointer != 0) {
|
||||
release(nativePointer);
|
||||
nativePointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected native long init(String detModelPath, String recModelPath, String clsModelPath, int useOpenCL, int threadNum, String cpuMode);
|
||||
|
||||
protected native float[] forward(long pointer, Bitmap originalImage, int max_size_len, int run_det, int run_cls, int run_rec);
|
||||
|
||||
protected native void release(long pointer);
|
||||
|
||||
private ArrayList<OcrResultModel> postprocess(float[] raw) {
|
||||
ArrayList<OcrResultModel> results = new ArrayList<>();
|
||||
int begin = 0;
|
||||
|
||||
while (begin < raw.length) {
|
||||
int point_num = Math.round(raw[begin]);
|
||||
int word_num = Math.round(raw[begin + 1]);
|
||||
OcrResultModel model = parse(raw, begin + 2, point_num, word_num);
|
||||
begin += 2 + 1 + point_num * 2 + word_num + 2;
|
||||
results.add(model);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private OcrResultModel parse(float[] raw, int begin, int pointNum, int wordNum) {
|
||||
int current = begin;
|
||||
OcrResultModel model = new OcrResultModel();
|
||||
model.setConfidence(raw[current]);
|
||||
current++;
|
||||
for (int i = 0; i < pointNum; i++) {
|
||||
model.addPoints(Math.round(raw[current + i * 2]), Math.round(raw[current + i * 2 + 1]));
|
||||
}
|
||||
current += (pointNum * 2);
|
||||
for (int i = 0; i < wordNum; i++) {
|
||||
int index = Math.round(raw[current + i]);
|
||||
model.addWordIndex(index);
|
||||
}
|
||||
current += wordNum;
|
||||
model.setClsIdx(raw[current]);
|
||||
model.setClsConfidence(raw[current + 1]);
|
||||
// Log.i("OCRPredictorNative", "word finished " + wordNum);
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.baidu.paddle.lite.ocr;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author PaddleOCR
|
||||
* Modified by TonyJiangWJ
|
||||
* @since 2023-08-06
|
||||
*/
|
||||
public class OcrResult implements Comparable<OcrResult> {
|
||||
private String label;
|
||||
private float confidence;
|
||||
private Rect bounds;
|
||||
private final List<OcrResult> elements = new ArrayList<>();
|
||||
|
||||
public OcrResult() {
|
||||
}
|
||||
|
||||
public OcrResult(OcrResultModel resultModel) {
|
||||
this.label = resultModel.getLabel();
|
||||
this.confidence = resultModel.getConfidence();
|
||||
int left = -1, right = -1, top = -1, bottom = -1;
|
||||
for (Point point : resultModel.getPoints()) {
|
||||
if (point.x < left || left == -1) {
|
||||
left = point.x;
|
||||
}
|
||||
if (point.x > right || right == -1) {
|
||||
right = point.x;
|
||||
}
|
||||
if (point.y < top || top == -1) {
|
||||
top = point.y;
|
||||
}
|
||||
if (point.y > bottom || bottom == -1) {
|
||||
bottom = point.y;
|
||||
}
|
||||
}
|
||||
this.bounds = new Rect(left, top, right, bottom);
|
||||
}
|
||||
|
||||
public OcrResult(String label, float confidence, Rect bounds) {
|
||||
this.label = label;
|
||||
this.confidence = confidence;
|
||||
this.bounds = bounds;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public float getConfidence() {
|
||||
return confidence;
|
||||
}
|
||||
|
||||
public void setConfidence(float confidence) {
|
||||
this.confidence = confidence;
|
||||
}
|
||||
|
||||
public Rect getBounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
public void setBounds(Rect bounds) {
|
||||
this.bounds = bounds;
|
||||
}
|
||||
|
||||
public RectLocation getLocation() {
|
||||
return new RectLocation(bounds);
|
||||
}
|
||||
|
||||
public String getWords() {
|
||||
return label.trim().replace("\r", "");
|
||||
}
|
||||
|
||||
public List<OcrResult> getElements() {
|
||||
return this.elements;
|
||||
}
|
||||
|
||||
public void addElements(OcrResult element) {
|
||||
this.elements.add(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(OcrResult o) {
|
||||
// 上下差距小于二分之一的高度 判定为同一行
|
||||
int deviation = Math.max(this.bounds.height(), o.bounds.height()) / 2;
|
||||
// 通过垂直中心点的距离判定
|
||||
if (Math.abs((this.bounds.top + this.bounds.bottom) / 2 - (o.bounds.top + o.bounds.bottom) / 2) < deviation) {
|
||||
return this.bounds.left - o.bounds.left;
|
||||
} else {
|
||||
return this.bounds.bottom - o.bounds.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OcrResult{" + "label='" + label + '\'' +
|
||||
", confidence=" + confidence +
|
||||
", bounds=" + bounds +
|
||||
", elements=" + elements +
|
||||
'}';
|
||||
}
|
||||
|
||||
public static class RectLocation {
|
||||
public int left;
|
||||
public int top;
|
||||
public int width;
|
||||
public int height;
|
||||
|
||||
public RectLocation() {
|
||||
}
|
||||
|
||||
public RectLocation(int left, int top, int width, int height) {
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public RectLocation(Rect rect) {
|
||||
left = rect.left;
|
||||
top = rect.top;
|
||||
width = rect.right - rect.left;
|
||||
height = rect.bottom - rect.top;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.baidu.paddle.lite.ocr;
|
||||
|
||||
import android.graphics.Point;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author PaddleOCR
|
||||
* Modified by TonyJiangWJ
|
||||
* @since 2023-08-06
|
||||
*/
|
||||
public class OcrResultModel {
|
||||
private final List<Point> points;
|
||||
private final List<Integer> wordIndex;
|
||||
private String label;
|
||||
private float confidence;
|
||||
private float clsIdx;
|
||||
private String clsLabel;
|
||||
private float clsConfidence;
|
||||
|
||||
public OcrResultModel() {
|
||||
super();
|
||||
points = new ArrayList<>();
|
||||
wordIndex = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void addPoints(int x, int y) {
|
||||
Point point = new Point(x, y);
|
||||
points.add(point);
|
||||
}
|
||||
|
||||
public void addWordIndex(int index) {
|
||||
wordIndex.add(index);
|
||||
}
|
||||
|
||||
public List<Point> getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
public List<Integer> getWordIndex() {
|
||||
return wordIndex;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public float getConfidence() {
|
||||
return confidence;
|
||||
}
|
||||
|
||||
public void setConfidence(float confidence) {
|
||||
this.confidence = confidence;
|
||||
}
|
||||
|
||||
public float getClsIdx() {
|
||||
return clsIdx;
|
||||
}
|
||||
|
||||
public void setClsIdx(float idx) {
|
||||
this.clsIdx = idx;
|
||||
}
|
||||
|
||||
public String getClsLabel() {
|
||||
return clsLabel;
|
||||
}
|
||||
|
||||
public void setClsLabel(String label) {
|
||||
this.clsLabel = label;
|
||||
}
|
||||
|
||||
public float getClsConfidence() {
|
||||
return clsConfidence;
|
||||
}
|
||||
|
||||
public void setClsConfidence(float confidence) {
|
||||
this.clsConfidence = confidence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OcrResultModel{" +
|
||||
"points=" + points +
|
||||
", wordIndex=" + wordIndex +
|
||||
", label='" + label + '\'' +
|
||||
", confidence=" + confidence +
|
||||
", clsIdx=" + clsIdx +
|
||||
", clsLabel='" + clsLabel + '\'' +
|
||||
", clsConfidence=" + clsConfidence +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.baidu.paddle.lite.ocr;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
public final class OpenCLGuard {
|
||||
|
||||
private static final String TAG = "OpenCLGuard";
|
||||
private static final String SP = "paddle_opencl_probe";
|
||||
private static final String KEY_CACHED_RES = "res_";
|
||||
private static final String KEY_CACHED_AT = "at_";
|
||||
// Timestamp of crash fuse.
|
||||
// zh-CN: 崩溃保险丝时间戳.
|
||||
private static final String KEY_LAST_FUSE = "fuse_";
|
||||
private static final long CACHE_TTL_MS = 24L * 60 * 60 * 1000; // 24h
|
||||
private static final long FUSE_MUTE_MS = 7L * 24 * 60 * 60 * 1000; // 7d
|
||||
|
||||
private static final String FINGERPRINT = android.os.Build.FINGERPRINT;
|
||||
private static final String CACHE_KEY_RES = KEY_CACHED_RES + FINGERPRINT;
|
||||
private static final String CACHE_KEY_AT = KEY_CACHED_AT + FINGERPRINT;
|
||||
private static final String FUSE_KEY = KEY_LAST_FUSE + FINGERPRINT;
|
||||
|
||||
private static final String[] CANDIDATES = new String[]{
|
||||
// Common Treble partitions.
|
||||
// zh-CN: 常见 Treble 分区.
|
||||
"/vendor/lib64/libOpenCL.so",
|
||||
"/system/lib64/libOpenCL.so",
|
||||
"/system/vendor/lib64/libOpenCL.so",
|
||||
"/odm/lib64/libOpenCL.so",
|
||||
// Some vendors put OpenCL in GPU APEX/extension area (not standard, just try).
|
||||
// zh-CN: 部分厂商会把 OpenCL 放到 GPU APEX/扩展区 (并不标准, 仅做尝试).
|
||||
"/apex/com.android.hwext/lib64/libOpenCL.so",
|
||||
// Some devices put ICD in a proprietary directory (rare).
|
||||
// zh-CN: 部分设备把 ICD 放在专有目录 (罕见).
|
||||
"/vendor/lib64/egl/libOpenCL.so"
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark: About to initialize OpenCL (if APP crashes, it can be detected next time).
|
||||
* zh-CN: 标记: 准备开始初始化 OpenCL (若 APP 崩溃, 下次就能检测到).
|
||||
*/
|
||||
public static void markInitStart(Context ctx) {
|
||||
ctx.getSharedPreferences(SP, 0).edit().putLong(FUSE_KEY, System.currentTimeMillis()).apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark: OpenCL initialization has safely ended (regardless of success or failure).
|
||||
* zh-CN: 标记: OpenCL 初始化已安全结束 (无论成功或失败).
|
||||
*/
|
||||
public static void markInitEnd(Context ctx) {
|
||||
ctx.getSharedPreferences(SP, 0).edit().remove(FUSE_KEY).apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to recommend enabling OpenCL (with cache + fuse + absolute path loading attempt).
|
||||
* zh-CN: 是否建议启用 OpenCL (带缓存 + 保险丝 + 绝对路径加载尝试).
|
||||
*/
|
||||
public static boolean isOpenCLRuntimeAvailable(Context ctx) {
|
||||
// Crash fuse: last initialization did not end normally -> pause for 7 days.
|
||||
// zh-CN: 崩溃保险丝: 上次初始化未正常结束 -> 暂停 7 天.
|
||||
long fuseTs = ctx.getSharedPreferences(SP, 0).getLong(FUSE_KEY, 0L);
|
||||
if (fuseTs > 0 && (System.currentTimeMillis() - fuseTs) < FUSE_MUTE_MS) {
|
||||
Log.w(TAG, "[OpenCL] Fuse active, skip probing.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only try in 64-bit process + arm64 device.
|
||||
// zh-CN: 只在 64-bit 进程 + arm64 设备尝试.
|
||||
boolean isArm64Device = false;
|
||||
try {
|
||||
String[] abis64 = Build.SUPPORTED_64_BIT_ABIS;
|
||||
if (abis64 != null) for (String abi : abis64) {
|
||||
if ("arm64-v8a".equalsIgnoreCase(abi)) {
|
||||
isArm64Device = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignore) {
|
||||
/* Ignored. */
|
||||
}
|
||||
boolean is64Process = System.getProperty("os.arch", "").contains("64");
|
||||
if (!(isArm64Device && is64Process)) {
|
||||
Log.i(TAG, "[OpenCL] Not arm64/64-bit process, skip. dev=" + isArm64Device + " proc=" + is64Process);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read cache.
|
||||
// zh-CN: 读取缓存.
|
||||
final var sp = ctx.getSharedPreferences(SP, 0);
|
||||
long cachedAt = sp.getLong(CACHE_KEY_AT, 0L);
|
||||
if (cachedAt > 0 && (System.currentTimeMillis() - cachedAt) < CACHE_TTL_MS) {
|
||||
boolean cached = sp.getBoolean(CACHE_KEY_RES, false);
|
||||
Log.i(TAG, "[OpenCL] use cached=" + cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 1) Absolute path existence.
|
||||
// zh-CN: 1) 绝对路径存在性.
|
||||
String hitPath = null;
|
||||
for (String p : CANDIDATES) {
|
||||
try {
|
||||
if (new java.io.File(p).exists()) {
|
||||
hitPath = p;
|
||||
break;
|
||||
}
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Try loading (absolute path first, then loadLibrary("OpenCL")).
|
||||
// zh-CN: 2) 尝试加载 (绝对路径优先, 其次 loadLibrary("OpenCL")).
|
||||
boolean loadOk = false;
|
||||
// 2.1 Absolute path dlopen.
|
||||
// zh-CN: 2.1 绝对路径 dlopen.
|
||||
if (hitPath != null) {
|
||||
try {
|
||||
System.load(hitPath);
|
||||
loadOk = true;
|
||||
Log.i(TAG, "[OpenCL] System.load hit: " + hitPath);
|
||||
} catch (Throwable t) {
|
||||
Log.i(TAG, "[OpenCL] System.load failed: " + hitPath + " -> " + t.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
// 2.2 Regular link name.
|
||||
// zh-CN: 2.2 常规链接名.
|
||||
if (!loadOk) {
|
||||
try {
|
||||
System.loadLibrary("OpenCL");
|
||||
loadOk = true;
|
||||
Log.i(TAG, "[OpenCL] loadLibrary(\"OpenCL\") ok.");
|
||||
} catch (Throwable t) {
|
||||
Log.i(TAG, "[OpenCL] loadLibrary(\"OpenCL\") failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
int probe = -999;
|
||||
if (loadOk) {
|
||||
try {
|
||||
probe = OpenCLProbe.nativeProbeOpenCL();
|
||||
} catch (Throwable t) {
|
||||
Log.i(TAG, "[OpenCL] nativeProbeOpenCL error: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
// At least 1 platform.
|
||||
// zh-CN: 至少 1 个平台.
|
||||
boolean available = loadOk && probe >= 1;
|
||||
Log.i(TAG, "[OpenCL] available=" + available + " (loadOk=" + loadOk + ", platforms=" + probe + ")");
|
||||
|
||||
// Write cache.
|
||||
// zh-CN: 写缓存.
|
||||
sp.edit().putBoolean(CACHE_KEY_RES, available).putLong(CACHE_KEY_AT, System.currentTimeMillis()).apply();
|
||||
return available;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.baidu.paddle.lite.ocr;
|
||||
|
||||
public final class OpenCLProbe {
|
||||
static {
|
||||
try { System.loadLibrary("opencl_probe"); } catch (Throwable ignore) {}
|
||||
}
|
||||
public static native int nativeProbeOpenCL();
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package com.baidu.paddle.lite.ocr
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import org.autojs.plugin.paddle.ocr.api.OcrOptions
|
||||
import org.autojs.plugin.paddle.ocr.api.OcrResult
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
/**
|
||||
* A unified embedded Paddle OCR engine API for both host (INRT) and plugin APK.
|
||||
* zh-CN: 面向宿主 (INRT) 与插件 APK 的统一 Paddle OCR 内置引擎 API.
|
||||
*
|
||||
* Created by JetBrains AI Assistant (GPT-5.2) on Jan 17, 2026.
|
||||
* Modified by SuperMonster003 as of Jan 18, 2026.
|
||||
*/
|
||||
class PaddleOcrEngine(
|
||||
private val appContext: Context,
|
||||
private val variant: VariantSpec,
|
||||
private val bridge: NativeBridge = PredictorNativeBridge(),
|
||||
) {
|
||||
|
||||
@Volatile
|
||||
private var initialized: Boolean = false
|
||||
|
||||
private val lock = Any()
|
||||
|
||||
/**
|
||||
* Initialize native libs and load models.
|
||||
* zh-CN: 初始化 native 库并加载模型.
|
||||
*/
|
||||
fun ensureInitialized(options: OcrOptions) {
|
||||
if (initialized) return
|
||||
synchronized(lock) {
|
||||
if (initialized) return
|
||||
|
||||
// Resolve model profile by options + variant.
|
||||
// zh-CN: 根据 options + variant 决定模型组合.
|
||||
val profile = variant.resolveProfile(options)
|
||||
|
||||
// Ensure required assets exist.
|
||||
// zh-CN: 确保所需 assets 存在.
|
||||
variant.assertAssetsExist(appContext, profile)
|
||||
|
||||
// Prepare checking bitmap from drawable resource.
|
||||
// zh-CN: 从 drawable 资源准备检查用 bitmap.
|
||||
val checkingBitmap = decodeDrawable(appContext, variant.checkingDrawableRes)
|
||||
|
||||
// Delegate to bridge for real initialization.
|
||||
// zh-CN: 委托给 bridge 执行真实初始化.
|
||||
bridge.init(
|
||||
context = appContext,
|
||||
profile = profile,
|
||||
checkingBitmap = checkingBitmap,
|
||||
)
|
||||
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognize text only.
|
||||
* zh-CN: 仅识别文本.
|
||||
*/
|
||||
fun recognizeText(bitmap: Bitmap, options: OcrOptions): List<String> {
|
||||
ensureInitialized(options)
|
||||
return bridge.recognizeText(bitmap, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect with boxes.
|
||||
* zh-CN: 检测并返回文本框信息.
|
||||
*/
|
||||
fun detect(bitmap: Bitmap, options: OcrOptions): List<OcrResult> {
|
||||
ensureInitialized(options)
|
||||
return bridge.detect(bitmap, options)
|
||||
}
|
||||
|
||||
private fun decodeDrawable(context: Context, resId: Int): Bitmap {
|
||||
return BitmapFactory.decodeResource(context.resources, resId)
|
||||
?: throw IllegalStateException(
|
||||
context.getString(R.string.error_failed_to_decode_checking_drawable_resource)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single point for real predictor/native invocation.
|
||||
* zh-CN: 真实 predictor/native 调用的单点封装.
|
||||
*/
|
||||
interface NativeBridge {
|
||||
|
||||
/**
|
||||
* Initialize predictor with model paths.
|
||||
* zh-CN: 用模型路径初始化 predictor.
|
||||
*/
|
||||
fun init(
|
||||
context: Context,
|
||||
profile: ModelProfile,
|
||||
checkingBitmap: Bitmap,
|
||||
)
|
||||
|
||||
/**
|
||||
* Recognize text.
|
||||
* zh-CN: 识别文本.
|
||||
*/
|
||||
fun recognizeText(bitmap: Bitmap, options: OcrOptions): List<String>
|
||||
|
||||
/**
|
||||
* Detect results with boxes.
|
||||
* zh-CN: 检测并返回带框结果.
|
||||
*/
|
||||
fun detect(bitmap: Bitmap, options: OcrOptions): List<OcrResult>
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved model paths for one run configuration.
|
||||
* zh-CN: 单次运行配置解析出的模型路径集合.
|
||||
*/
|
||||
data class ModelProfile(
|
||||
val variantName: String,
|
||||
val labelAssetPath: String,
|
||||
|
||||
// v5 uses Predictor's internal default dirs; v3 uses explicit dir.
|
||||
// zh-CN: v5 由 Predictor 内部默认目录决定, v3 使用显式目录.
|
||||
val modelDir: String?,
|
||||
|
||||
val useSlim: Boolean,
|
||||
val useOpenCL: Boolean,
|
||||
val cpuThreadNum: Int,
|
||||
val detModelFile: String,
|
||||
val recModelFile: String,
|
||||
val clsModelFile: String,
|
||||
val assetsToCheck: List<String>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Engine variant specification (v3/v5).
|
||||
* zh-CN: 引擎变体规范 (v3/v5).
|
||||
*/
|
||||
data class VariantSpec(
|
||||
val name: String,
|
||||
val supportsOpenCL: Boolean,
|
||||
val labelAssetPath: String,
|
||||
|
||||
// Explicit model directories.
|
||||
// zh-CN: 显式模型目录配置.
|
||||
|
||||
val modelDirCpu: String,
|
||||
val modelDirCpuSlim: String? = null,
|
||||
val modelDirOpenCL: String? = null,
|
||||
val modelDirOpenCLSlim: String? = null,
|
||||
|
||||
val detModelFile: String,
|
||||
val recModelFile: String,
|
||||
val clsModelFile: String,
|
||||
|
||||
val checkingDrawableRes: Int,
|
||||
) {
|
||||
|
||||
fun resolveProfile(options: OcrOptions): ModelProfile {
|
||||
val useSlim = options.useSlim
|
||||
val useOpenCLRequested = options.useOpenCL && supportsOpenCL
|
||||
|
||||
val resolvedDir: String? = if (name == NAME_V5) {
|
||||
// v5: let Predictor decide between CPU/OpenCL + fallback internally.
|
||||
// zh-CN: v5: 让 Predictor 内部决定 CPU/OpenCL 并自动回退.
|
||||
null
|
||||
} else {
|
||||
// v3: choose deterministic dir here (OpenCL ignored).
|
||||
// zh-CN: v3: 在此确定性选择目录 (OpenCL 被忽略).
|
||||
if (useSlim) (modelDirCpuSlim ?: modelDirCpu) else modelDirCpu
|
||||
}
|
||||
|
||||
// Assets to check:
|
||||
// - Always check label file.
|
||||
// - For v3: check the resolved model dir files.
|
||||
// - For v5: check CPU + slim dirs always, and additionally OpenCL dirs if requested.
|
||||
// zh-CN:
|
||||
// - 总是检查 label 文件.
|
||||
// - v3: 检查解析出的模型目录及其文件.
|
||||
// - v5: 总是检查 CPU/INT8 目录, 若用户请求 OpenCL 再额外检查 OpenCL 目录.
|
||||
val assets = buildList {
|
||||
add(labelAssetPath)
|
||||
|
||||
val cpuDir = modelDirCpu
|
||||
add("$cpuDir/$detModelFile")
|
||||
add("$cpuDir/$recModelFile")
|
||||
add("$cpuDir/$clsModelFile")
|
||||
|
||||
modelDirCpuSlim?.let { slimDir ->
|
||||
add("$slimDir/$detModelFile")
|
||||
add("$slimDir/$recModelFile")
|
||||
add("$slimDir/$clsModelFile")
|
||||
}
|
||||
|
||||
if (useOpenCLRequested) {
|
||||
modelDirOpenCL?.let { clDir ->
|
||||
add("$clDir/$detModelFile")
|
||||
add("$clDir/$recModelFile")
|
||||
add("$clDir/$clsModelFile")
|
||||
}
|
||||
modelDirOpenCLSlim?.let { clSlimDir ->
|
||||
add("$clSlimDir/$detModelFile")
|
||||
add("$clSlimDir/$recModelFile")
|
||||
add("$clSlimDir/$clsModelFile")
|
||||
}
|
||||
}
|
||||
|
||||
// v3 deterministic dir check (override list to minimal set).
|
||||
// zh-CN: v3 确定性目录检查 (覆盖为最小集合).
|
||||
if (name != NAME_V5) {
|
||||
clear()
|
||||
add(labelAssetPath)
|
||||
val dir = requireNotNull(resolvedDir)
|
||||
add("$dir/$detModelFile")
|
||||
add("$dir/$recModelFile")
|
||||
add("$dir/$clsModelFile")
|
||||
}
|
||||
}
|
||||
|
||||
return ModelProfile(
|
||||
variantName = name,
|
||||
labelAssetPath = labelAssetPath,
|
||||
modelDir = resolvedDir,
|
||||
useSlim = useSlim,
|
||||
useOpenCL = useOpenCLRequested,
|
||||
cpuThreadNum = options.cpuThreadNum,
|
||||
detModelFile = detModelFile,
|
||||
recModelFile = recModelFile,
|
||||
clsModelFile = clsModelFile,
|
||||
assetsToCheck = assets.distinct(),
|
||||
)
|
||||
}
|
||||
|
||||
fun assertAssetsExist(context: Context, profile: ModelProfile) {
|
||||
fun assertOne(path: String) {
|
||||
try {
|
||||
context.assets.open(path).use { }
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw IllegalStateException(context.getString(R.string.error_missing_required_paddle_ocr_asset, path), e)
|
||||
}
|
||||
}
|
||||
profile.assetsToCheck.forEach(::assertOne)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NAME_V5 = "v5"
|
||||
const val NAME_V3 = "v3"
|
||||
|
||||
fun v5(): VariantSpec = VariantSpec(
|
||||
name = NAME_V5,
|
||||
supportsOpenCL = true,
|
||||
labelAssetPath = "labels/ppocr_keys_ocrv5.txt",
|
||||
|
||||
modelDirCpu = "models/pp-ocrv5-arm",
|
||||
modelDirCpuSlim = "models/pp-ocrv5-arm-int8",
|
||||
modelDirOpenCL = "models/pp-ocrv5-arm-opencl",
|
||||
modelDirOpenCLSlim = "models/pp-ocrv5-arm-opencl-int8",
|
||||
|
||||
detModelFile = "PP-OCRv5_mobile_det.nb",
|
||||
recModelFile = "PP-OCRv5_mobile_rec.nb",
|
||||
clsModelFile = "PP-LCNet_x1_0_textline_ori.nb",
|
||||
|
||||
checkingDrawableRes = R.drawable.paddle_ocr_test,
|
||||
)
|
||||
|
||||
fun v3(): VariantSpec = VariantSpec(
|
||||
name = NAME_V3,
|
||||
supportsOpenCL = false,
|
||||
labelAssetPath = "labels/ppocr_keys_v1.txt",
|
||||
|
||||
modelDirCpu = "models/ocr_v3_for_cpu",
|
||||
modelDirCpuSlim = "models/ocr_v3_for_cpu(slim)",
|
||||
|
||||
modelDirOpenCL = null,
|
||||
modelDirOpenCLSlim = null,
|
||||
|
||||
detModelFile = "det_opt.nb",
|
||||
recModelFile = "rec_opt.nb",
|
||||
clsModelFile = "cls_opt.nb",
|
||||
|
||||
checkingDrawableRes = R.drawable.paddle_ocr_test,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
package com.baidu.paddle.lite.ocr;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Build;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import org.opencv.BuildConfig;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Predictor for Paddle-Lite OCR engine, managing model loading, runtime configuration,
|
||||
* and end-to-end inference flow.
|
||||
* zh-CN: 面向 Paddle-Lite OCR 引擎的预测器, 负责模型加载/运行时配置以及端到端推理流程.
|
||||
*
|
||||
* @author <a href="https://github.com/TonyJiangWJ">TonyJiangWJ</a>
|
||||
* @see <a href="https://github.com/PaddlePaddle/PaddleOCR/blob/main/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Predictor.java">
|
||||
* PaddlePaddle/PaddleOCR (Predictor.java)</a>
|
||||
* @since Aug 6, 2023
|
||||
*
|
||||
* <p> Modified by TonyJiangWJ as of Aug 7, 2023. </p>
|
||||
* <p> Modified by JetBrains AI Assistant (GPT-5.2) as of Jan 17, 2026. </p>
|
||||
* <p> Modified by SuperMonster003 as of Jan 18, 2026. </p>
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class Predictor {
|
||||
|
||||
public static final int DEFAULT_CPU_THREAD_NUM = 4;
|
||||
public static final boolean DEFAULT_USE_SLIM = true;
|
||||
public static final boolean DEFAULT_USE_OPENCL = false;
|
||||
|
||||
private static final String TAG = Predictor.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Probe bitmap cache for init-check.
|
||||
* zh-CN: 初始化校验使用的探测位图缓存.
|
||||
*/
|
||||
private static Bitmap checkingBitmap;
|
||||
/**
|
||||
* Default label file path.
|
||||
* zh-CN: 默认字典文件路径.
|
||||
*/
|
||||
private final String defaultLabelPath = "labels/ppocr_keys_ocrv5.txt";
|
||||
/**
|
||||
* Default CPU model directory (standard).
|
||||
* zh-CN: 默认 CPU 标准模型目录.
|
||||
*/
|
||||
private final String defaultModelPath = "models/pp-ocrv5-arm";
|
||||
/**
|
||||
* Default OpenCL model directory (standard).
|
||||
* zh-CN: 默认 OpenCL 标准模型目录.
|
||||
*/
|
||||
private final String defaultModelPathOpenCL = "models/pp-ocrv5-arm-opencl";
|
||||
/**
|
||||
* Default CPU model directory (INT8 slim).
|
||||
* zh-CN: 默认 CPU INT8 slim 模型目录.
|
||||
*/
|
||||
private final String defaultModelPathSlim = "models/pp-ocrv5-arm-int8";
|
||||
/**
|
||||
* Default OpenCL model directory (INT8 slim).
|
||||
* zh-CN: 默认 OpenCL INT8 slim 模型目录.
|
||||
*/
|
||||
private final String defaultModelPathOpenCLSlim = "models/pp-ocrv5-arm-opencl-int8";
|
||||
|
||||
/** Detection model. [zh-CN: 检测模型]. */
|
||||
public String detModelFilename = "PP-OCRv5_mobile_det.nb";
|
||||
/** Recognition model. [zh-CN: 识别模型]. */
|
||||
public String recModelFilename = "PP-OCRv5_mobile_rec.nb";
|
||||
/** Text direction (cls) model. [zh-CN: 方向分类 (cls) 模型]. */
|
||||
public String clsModelFilename = "PP-LCNet_x1_0_textline_ori.nb";
|
||||
|
||||
/** Whether the model is loaded. [zh-CN: 模型是否已加载]. */
|
||||
public boolean isLoaded = false;
|
||||
/** Warm-up iteration count. [zh-CN: 预热迭代次数]. */
|
||||
public int warmupIterNum = 1;
|
||||
/** Inference iteration count for timing. [zh-CN: 用于计时的推理迭代次数]. */
|
||||
public int inferIterNum = 1;
|
||||
/** CPU thread count. [zh-CN: CPU 线程数]. */
|
||||
public int cpuThreadNum = DEFAULT_CPU_THREAD_NUM;
|
||||
/** CPU power mode string (Lite power hint). [zh-CN: CPU 能耗模式字符串 (Lite 电源提示)]. */
|
||||
public String cpuPowerMode = "LITE_POWER_HIGH";
|
||||
|
||||
/** Selected model resolved absolute path. [zh-CN: 选定模型解析后的绝对路径]. */
|
||||
public String modelPath = "";
|
||||
/** Selected model directory name. [zh-CN: 选定模型目录名]. */
|
||||
public String modelName = "";
|
||||
|
||||
/** Use slim (INT8) model. [zh-CN: 是否使用 slim (INT8) 模型]. */
|
||||
public boolean useSlim = DEFAULT_USE_SLIM;
|
||||
/** Use OpenCL backend (if available). [zh-CN: 是否启用 OpenCL 后端 (若可用)]. */
|
||||
public boolean useOpenCL = DEFAULT_USE_OPENCL;
|
||||
/** Validate initialization with a preset image. [zh-CN: 是否通过预设图片校验初始化]. */
|
||||
public boolean checkModelLoaded = BuildConfig.DEBUG;
|
||||
|
||||
/** Enable classification (cls). [zh-CN: 启用方向分类 (cls)]. */
|
||||
public boolean isClassificationEnabled = false;
|
||||
/** Enable detection (det). [zh-CN: 启用文本检测 (det)]. */
|
||||
public boolean isDetectionEnabled = false;
|
||||
/** Enable recognition (rec). [zh-CN: 启用文本识别 (rec)]. */
|
||||
public boolean isRecognitionEnabled = true;
|
||||
|
||||
/** Score threshold for filtering results. [zh-CN: 结果过滤的置信度阈值]. */
|
||||
public float scoreThreshold = 0.1f;
|
||||
/** Max long side for det input resize. [zh-CN: 检测输入缩放的最长边]. */
|
||||
protected int detLongSize = 960;
|
||||
|
||||
/** Native predictor bridge. [zh-CN: Native 预测器桥接对象]. */
|
||||
protected OCRPredictorNative paddlePredictor = null;
|
||||
/** Inference time in milliseconds. [zh-CN: 推理耗时 (毫秒)]. */
|
||||
protected float inferenceTime = 0;
|
||||
/** Labels for recognition post-processing. [zh-CN: 识别后处理所需的字典标签]. */
|
||||
protected List<String> wordLabels = new ArrayList<>();
|
||||
/** Input image buffer (ARGB_8888). [zh-CN: 输入图像缓冲 (ARGB_8888)]. */
|
||||
protected Bitmap inputImage = null;
|
||||
/** Preprocess time in milliseconds. [zh-CN: 预处理耗时 (毫秒)]. */
|
||||
protected float preprocessTime = 0;
|
||||
|
||||
/** Validation attempt counter. [zh-CN: 初始化校验重试计数器]. */
|
||||
private int validationAttempt = 1;
|
||||
/** Initialization attempt counter. [zh-CN: 初始化尝试计数器]. */
|
||||
private int initializationAttempt = 1;
|
||||
|
||||
// @Archived by SuperMonster003 on Nov 7, 2025.
|
||||
// ! Legacy default paths and filenames for PP-OCRv3 are archived here.
|
||||
// ! zh-CN: 旧版 PP-OCRv3 的默认路径与文件名在此归档保留.
|
||||
// # private final String defaultLabelPath = "labels/ppocr_keys_v1.txt";
|
||||
// # private final String defaultModelPath = "models/ocr_v3_for_cpu";
|
||||
// # public String detModelFilename = "det_opt.nb";
|
||||
// # public String recModelFilename = "rec_opt.nb";
|
||||
// # public String clsModelFilename = "cls_opt.nb";
|
||||
// # // Slim model converted by opt 2.10; 2.11 had issues.
|
||||
// # // zh-CN: Slim 模型由 2.10 版 opt 转换; 2.11 存在兼容问题.
|
||||
// # private final String defaultModelPathSlim = "models/ocr_v3_for_cpu(slim)";
|
||||
|
||||
public Predictor() {
|
||||
/* Empty body. */
|
||||
}
|
||||
|
||||
public static String md5(String text) {
|
||||
MessageDigest md;
|
||||
byte[] bytesOfMessage = text.getBytes();
|
||||
try {
|
||||
md = MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return Base64.encodeToString(md.digest(bytesOfMessage), Base64.DEFAULT);
|
||||
}
|
||||
|
||||
public boolean init(Context appCtx) {
|
||||
return this.init(appCtx, defaultModelPath, defaultLabelPath);
|
||||
}
|
||||
|
||||
public boolean init(Context appCtx, boolean useSlim) {
|
||||
return this.init(appCtx, useSlim, DEFAULT_USE_OPENCL);
|
||||
}
|
||||
|
||||
public boolean init(Context appCtx, boolean useSlim, boolean useOpenCL) {
|
||||
Log.d(TAG, "use slim: " + useSlim);
|
||||
Log.d(TAG, "use opencl: " + useOpenCL);
|
||||
|
||||
// If already loaded and switches are consistent, reuse directly.
|
||||
// zh-CN: 若已加载且开关一致, 直接复用.
|
||||
if (this.isLoaded && this.useSlim == useSlim && this.useOpenCL == useOpenCL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean openclAvailable = false;
|
||||
if (useOpenCL) {
|
||||
try {
|
||||
openclAvailable = OpenCLGuard.isOpenCLRuntimeAvailable(appCtx);
|
||||
if (!openclAvailable) {
|
||||
Log.w(TAG, "[OpenCL] Unavailable or fused, fallback to CPU.");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "[OpenCL] Probe exception, fallback to CPU: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
this.useSlim = useSlim;
|
||||
this.useOpenCL = openclAvailable;
|
||||
|
||||
String modelDir;
|
||||
if (this.useSlim) {
|
||||
modelDir = this.useOpenCL ? defaultModelPathOpenCLSlim : defaultModelPathSlim;
|
||||
} else {
|
||||
modelDir = this.useOpenCL ? defaultModelPathOpenCL : defaultModelPath;
|
||||
}
|
||||
|
||||
boolean ok;
|
||||
if (this.useOpenCL) {
|
||||
OpenCLGuard.markInitStart(appCtx);
|
||||
try {
|
||||
ok = this.init(appCtx, modelDir, defaultLabelPath);
|
||||
} finally {
|
||||
OpenCLGuard.markInitEnd(appCtx);
|
||||
}
|
||||
if (!ok) {
|
||||
Log.w(TAG, "[OpenCL] Init failed without crash, fallback to CPU.");
|
||||
this.useOpenCL = false;
|
||||
String cpuModelDir = this.useSlim ? defaultModelPathSlim : defaultModelPath;
|
||||
ok = this.init(appCtx, cpuModelDir, defaultLabelPath);
|
||||
}
|
||||
return ok;
|
||||
} else {
|
||||
return this.init(appCtx, modelDir, defaultLabelPath);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean init(Context appCtx, String modelPath, String labelPath) {
|
||||
Log.d(TAG, "init whit model: " + modelPath + " label: " + labelPath);
|
||||
isLoaded = loadModel(appCtx, modelPath, cpuThreadNum, cpuPowerMode);
|
||||
if (!isLoaded) {
|
||||
return false;
|
||||
}
|
||||
isLoaded = loadLabel(appCtx, labelPath);
|
||||
if (!checkModelLoadedSuccess(appCtx)) {
|
||||
if (initializationAttempt++ < 3) {
|
||||
return init(appCtx, modelPath, labelPath);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return isLoaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and validate models by running inference on a preset test image.
|
||||
* Retry up to several times as a workaround (deeper cause needs investigation).
|
||||
* zh-CN:
|
||||
* 初始化模型后通过识别预设图片校验是否初始化成功.
|
||||
* 曲线救国, 深层的失败原因需要后续排查.
|
||||
*/
|
||||
private boolean checkModelLoadedSuccess(Context context) {
|
||||
if (!checkModelLoaded) {
|
||||
return true;
|
||||
}
|
||||
if (!isLoaded) {
|
||||
return false;
|
||||
}
|
||||
List<OcrResult> results = runOcr(getCheckingBitmap(context));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (OcrResult result : results) {
|
||||
sb.append(result.getLabel());
|
||||
}
|
||||
// The image contains a single recognizable text string "测试" (Chinese word "test").
|
||||
// zh-CN: 图片中包含唯一可识别文本 "测试".
|
||||
boolean check = sb.toString().contains("测试");
|
||||
Log.d(TAG, "Validation attempt " + validationAttempt + ": { initialized: " + check + ", result: " + sb + " }");
|
||||
boolean result = check || validationAttempt++ >= 5;
|
||||
if (!check && validationAttempt >= 5) {
|
||||
Log.e(TAG, "Model initialization failed");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Bitmap getCheckingBitmap(Context context) {
|
||||
if (checkingBitmap == null) {
|
||||
checkingBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.paddle_ocr_test);
|
||||
}
|
||||
return checkingBitmap;
|
||||
}
|
||||
|
||||
public boolean init(Context appCtx, String modelPath, String labelPath, int cpuThreadNum, String cpuPowerMode) {
|
||||
isLoaded = loadModel(appCtx, modelPath, cpuThreadNum, cpuPowerMode);
|
||||
if (!isLoaded) {
|
||||
return false;
|
||||
}
|
||||
isLoaded = loadLabel(appCtx, labelPath);
|
||||
return isLoaded;
|
||||
}
|
||||
|
||||
public boolean init(Context appCtx, String modelPath, String labelPath, int cpuThreadNum, String cpuPowerMode, int detLongSize, float scoreThreshold) {
|
||||
boolean isLoaded = init(appCtx, modelPath, labelPath, cpuThreadNum, cpuPowerMode);
|
||||
if (!isLoaded) {
|
||||
return false;
|
||||
}
|
||||
this.detLongSize = detLongSize;
|
||||
this.scoreThreshold = scoreThreshold;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean loadModel(Context appCtx, String modelPath, int cpuThreadNum, String cpuPowerMode) {
|
||||
// Release model if exists.
|
||||
// zh-CN: 释放模型如果存在.
|
||||
releaseModel();
|
||||
|
||||
// Load model.
|
||||
// zh-CN: 加载模型.
|
||||
if (modelPath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String realPath = modelPath;
|
||||
if (modelPath.charAt(0) != '/') {
|
||||
// Read model files from custom path if the first character of mode path is '/'
|
||||
// otherwise copy model to cache from assets.
|
||||
// zh-CN: 如果模型路径首字符为 '/' 则从自定义路径读取模型文件, 否则从 assets 复制模型到缓存.
|
||||
realPath = appCtx.getCacheDir() + File.separator + modelPath;
|
||||
|
||||
// @SectionBegin("copyModelAssets") by TonyJiangWJ on Aug 7, 2023.
|
||||
String key = "PADDLE_MODEL_LOADED" + md5(modelPath);
|
||||
// Model has been updated, force override the old model.
|
||||
// zh-CN: 进行了模型更新, 需要强制覆盖旧模型.
|
||||
boolean loaded = PreferenceManager.getDefaultSharedPreferences(appCtx).getBoolean(key, false);
|
||||
if (loaded) {
|
||||
// No need to copy every time.
|
||||
// zh-CN: 没有必要每次都复制.
|
||||
Utils.copyDirectoryFromAssetsIfNeeded(appCtx, modelPath, realPath);
|
||||
} else {
|
||||
Utils.copyDirectoryFromAssets(appCtx, modelPath, realPath);
|
||||
PreferenceManager.getDefaultSharedPreferences(appCtx).edit().putBoolean(key, true).apply();
|
||||
}
|
||||
// @SectionEnd("copyModelAssets")
|
||||
}
|
||||
|
||||
OCRPredictorNative.Config config = new OCRPredictorNative.Config();
|
||||
|
||||
// Whether to use GPU (OpenCL), only set to 1 when useOpenCL is confirmed available at Java level.
|
||||
// zh-CN: 是否使用 GPU (OpenCL), 只有在 Java 层确认 useOpenCL 可用时才真正置 1.
|
||||
config.useOpenCL = useOpenCL ? 1 : 0;
|
||||
config.cpuThreadNum = cpuThreadNum;
|
||||
config.detModelFilename = realPath + File.separator + detModelFilename;
|
||||
config.recModelFilename = realPath + File.separator + recModelFilename;
|
||||
config.clsModelFilename = realPath + File.separator + clsModelFilename;
|
||||
Log.i("Predictor", "model path" + config.detModelFilename + " ; " + config.recModelFilename + ";" + config.clsModelFilename);
|
||||
config.cpuPower = cpuPowerMode;
|
||||
|
||||
paddlePredictor = new OCRPredictorNative(config);
|
||||
|
||||
this.cpuThreadNum = cpuThreadNum;
|
||||
this.cpuPowerMode = cpuPowerMode;
|
||||
this.modelPath = realPath;
|
||||
this.modelName = realPath.substring(realPath.lastIndexOf(File.separator) + 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void releaseModel() {
|
||||
if (paddlePredictor != null) {
|
||||
paddlePredictor.destroy();
|
||||
paddlePredictor = null;
|
||||
}
|
||||
isLoaded = false;
|
||||
modelPath = "";
|
||||
modelName = "";
|
||||
}
|
||||
|
||||
protected boolean loadLabel(Context appCtx, String labelPath) {
|
||||
wordLabels.clear();
|
||||
wordLabels.add("black");
|
||||
// Load word labels from file.
|
||||
// zh-CN: 从文件中加载字典标签.
|
||||
try {
|
||||
InputStream labelInputStream;
|
||||
if (labelPath.startsWith(File.separator)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
labelInputStream = Files.newInputStream(Paths.get(labelPath));
|
||||
} else {
|
||||
labelInputStream = new FileInputStream(labelPath);
|
||||
}
|
||||
} else {
|
||||
labelInputStream = appCtx.getAssets().open(labelPath);
|
||||
}
|
||||
int available = labelInputStream.available();
|
||||
byte[] lines = new byte[available];
|
||||
if (labelInputStream.read(lines) <= 0) {
|
||||
Log.e(TAG, "Failed to read label");
|
||||
return false;
|
||||
}
|
||||
labelInputStream.close();
|
||||
String words = new String(lines);
|
||||
// Compatible with \r\n line endings on Windows.
|
||||
// zh-CN: 兼容 Windows 系统下的 \r\n 换行符.
|
||||
String[] contents = words.split("(\r)?\n");
|
||||
wordLabels.addAll(Arrays.asList(contents));
|
||||
wordLabels.add(" ");
|
||||
Log.i(TAG, "Word label size: " + wordLabels.size());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<OcrResult> runOcr(Bitmap inputImage) {
|
||||
if (inputImage == null || !isLoaded()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
int run_det = isDetectionEnabled ? 1 : 0;
|
||||
int run_cls = isClassificationEnabled ? 1 : 0;
|
||||
int run_rec = isRecognitionEnabled ? 1 : 0;
|
||||
// Warm up.
|
||||
// zh-CN: 预热.
|
||||
for (int i = 0; i < warmupIterNum; i++) {
|
||||
paddlePredictor.runImage(inputImage, detLongSize, run_det, run_cls, run_rec);
|
||||
}
|
||||
// Do not need warm.
|
||||
// zh-CN: 不需要预热.
|
||||
warmupIterNum = 0;
|
||||
// Run inference.
|
||||
// zh-CN: 执行推理.
|
||||
Date start = new Date();
|
||||
ArrayList<OcrResultModel> results = paddlePredictor.runImage(inputImage, detLongSize, run_det, run_cls, run_rec);
|
||||
Date end = new Date();
|
||||
inferenceTime = (end.getTime() - start.getTime()) / (float) inferIterNum;
|
||||
|
||||
postProcess(results);
|
||||
Log.i(TAG, "[stat] Preprocess Time: " + preprocessTime + "; Inference Time: " + inferenceTime + "; Box Size: " + results.size());
|
||||
List<OcrResult> ocrResults = new ArrayList<>();
|
||||
for (OcrResultModel resultModel : results) {
|
||||
// Log.d(TAG, "Recognize: " + resultModel);
|
||||
if (resultModel.getConfidence() >= scoreThreshold) {
|
||||
ocrResults.add(new OcrResult(resultModel));
|
||||
} else {
|
||||
// Log.d(TAG, "Discard: " + resultModel);
|
||||
}
|
||||
}
|
||||
Collections.sort(ocrResults);
|
||||
return ocrResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether model is loaded and predictor is valid.
|
||||
* zh-CN: 模型是否已加载且预测器有效.
|
||||
*/
|
||||
public boolean isLoaded() {
|
||||
return paddlePredictor != null && isLoaded;
|
||||
}
|
||||
|
||||
public String modelPath() {
|
||||
return modelPath;
|
||||
}
|
||||
|
||||
public String modelName() {
|
||||
return modelName;
|
||||
}
|
||||
|
||||
public int cpuThreadNum() {
|
||||
return cpuThreadNum;
|
||||
}
|
||||
|
||||
public String cpuPowerMode() {
|
||||
return cpuPowerMode;
|
||||
}
|
||||
|
||||
public float inferenceTime() {
|
||||
return inferenceTime;
|
||||
}
|
||||
|
||||
public Bitmap inputImage() {
|
||||
return inputImage;
|
||||
}
|
||||
|
||||
public float preprocessTime() {
|
||||
return preprocessTime;
|
||||
}
|
||||
|
||||
public String getDefaultLabelPath() {
|
||||
return defaultLabelPath;
|
||||
}
|
||||
|
||||
public String getDefaultModelPath() {
|
||||
return defaultModelPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default OpenCL model directory (standard).
|
||||
* zh-CN: 获取默认的 OpenCL 标准模型目录.
|
||||
*/
|
||||
public String getDefaultModelPathOpenCL() {
|
||||
return defaultModelPathOpenCL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default CPU model directory (INT8 slim).
|
||||
* zh-CN: 获取默认的 CPU INT8 slim 模型目录.
|
||||
*/
|
||||
public String getDefaultModelPathSlim() {
|
||||
return defaultModelPathSlim;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default OpenCL model directory (INT8 slim).
|
||||
* zh-CN: 获取默认的 OpenCL INT8 slim 模型目录.
|
||||
*/
|
||||
public String getDefaultModelPathOpenCLSlim() {
|
||||
return defaultModelPathOpenCLSlim;
|
||||
}
|
||||
|
||||
public boolean isUseSlim() {
|
||||
return useSlim;
|
||||
}
|
||||
|
||||
public boolean isUseOpenCL() {
|
||||
return useOpenCL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set input image buffer (copy to ARGB_8888).
|
||||
* zh-CN: 设置输入图像缓冲 (复制为 ARGB_8888).
|
||||
*/
|
||||
public void setInputImage(Bitmap image) {
|
||||
if (image != null) {
|
||||
this.inputImage = image.copy(Bitmap.Config.ARGB_8888, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert raw recognition outputs to text labels and metadata.
|
||||
* zh-CN: 将识别原始输出转换为文本标签及元数据.
|
||||
*/
|
||||
private void postProcess(ArrayList<OcrResultModel> results) {
|
||||
for (OcrResultModel r : results) {
|
||||
StringBuilder word = new StringBuilder();
|
||||
for (int index : r.getWordIndex()) {
|
||||
if (index >= 0 && index < wordLabels.size()) {
|
||||
word.append(wordLabels.get(index));
|
||||
} else {
|
||||
Log.e(TAG, "Word index is not in label list:" + index);
|
||||
word.append(" ");
|
||||
}
|
||||
}
|
||||
r.setLabel(word.toString());
|
||||
r.setClsLabel(r.getClsIdx() == 1 ? "180" : "0");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable classification (cls).
|
||||
* zh-CN: 启用/禁用方向分类 (cls).
|
||||
*/
|
||||
public void setClassificationEnabled(boolean enable) {
|
||||
this.isClassificationEnabled = enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable detection (det).
|
||||
* zh-CN: 启用/禁用文本检测 (det).
|
||||
*/
|
||||
public void setDetectionEnabled(boolean enable) {
|
||||
this.isDetectionEnabled = enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable recognition (rec).
|
||||
* zh-CN: 启用/禁用文本识别 (rec).
|
||||
*/
|
||||
public void setRecognitionEnabled(boolean enable) {
|
||||
this.isRecognitionEnabled = enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set max long side for detection input (smaller is usually faster, e.g., 736-960).
|
||||
* zh-CN: 设置检测输入的最长边 (越小通常越快, 例如 736-960).
|
||||
*/
|
||||
public void setDetLongSize(int detLongSize) {
|
||||
this.detLongSize = detLongSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set score threshold (slightly improves speed by pruning noisy boxes).
|
||||
* zh-CN: 设置置信度阈值 (通过剔除噪声框可略微提速).
|
||||
*/
|
||||
public void setScoreThreshold(float scoreThreshold) {
|
||||
this.scoreThreshold = scoreThreshold;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.baidu.paddle.lite.ocr
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import org.autojs.plugin.paddle.ocr.api.OcrOptions
|
||||
import org.autojs.plugin.paddle.ocr.api.OcrResult
|
||||
|
||||
/**
|
||||
* Native bridge based on com.baidu.paddle.lite.ocr.Predictor.
|
||||
* zh-CN: 基于 com.baidu.paddle.lite.ocr.Predictor 的 native bridge.
|
||||
*
|
||||
* Created by JetBrains AI Assistant (GPT-5.2) on Jan 17, 2026.
|
||||
* Modified by SuperMonster003 as of Jan 18, 2026.
|
||||
*/
|
||||
class PredictorNativeBridge : NativeBridge {
|
||||
|
||||
private val predictor = Predictor()
|
||||
|
||||
@Volatile
|
||||
private var lastProfileKey: String? = null
|
||||
|
||||
override fun init(context: Context, profile: ModelProfile, checkingBitmap: Bitmap) {
|
||||
|
||||
val desiredThreadNum = profile.cpuThreadNum
|
||||
val desiredUseSlim = profile.useSlim
|
||||
val desiredUseOpenCL = profile.useOpenCL
|
||||
|
||||
// Compute a simple cache key to avoid redundant re-init.
|
||||
// zh-CN: 计算简单缓存 key, 避免重复初始化.
|
||||
val key = "${profile.variantName}|t=$desiredThreadNum|slim=$desiredUseSlim|opencl=$desiredUseOpenCL|dir=${profile.modelDir ?: "-"}"
|
||||
if (predictor.isLoaded && lastProfileKey == key) return
|
||||
|
||||
// Predictor.init() may do heavy work and should not block main thread.
|
||||
// zh-CN: Predictor.init() 可能较耗时, 不应阻塞主线程.
|
||||
val ok = if (Looper.getMainLooper() == Looper.myLooper()) {
|
||||
val lock = Object()
|
||||
val completed = booleanArrayOf(false)
|
||||
val initResult = booleanArrayOf(false)
|
||||
|
||||
Thread {
|
||||
initResult[0] = initInternal(context, profile)
|
||||
synchronized(lock) {
|
||||
completed[0] = true
|
||||
lock.notifyAll()
|
||||
}
|
||||
}.start()
|
||||
|
||||
val deadline = System.currentTimeMillis() + 60_000
|
||||
var interrupted = false
|
||||
synchronized(lock) {
|
||||
try {
|
||||
while (!completed[0]) {
|
||||
val remaining = deadline - System.currentTimeMillis()
|
||||
if (remaining <= 0) break
|
||||
lock.wait(remaining)
|
||||
}
|
||||
} catch (_: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
interrupted = true
|
||||
}
|
||||
}
|
||||
!interrupted && completed[0] && initResult[0]
|
||||
} else {
|
||||
initInternal(context, profile)
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
throw IllegalStateException(context.getString(R.string.error_failed_to_initialize_paddle_ocr_predictor))
|
||||
}
|
||||
|
||||
lastProfileKey = key
|
||||
}
|
||||
|
||||
private fun initInternal(context: Context, profile: ModelProfile): Boolean {
|
||||
// Ensure cpuThreadNum updates take effect.
|
||||
// zh-CN: 确保 cpuThreadNum 更新生效.
|
||||
if (predictor.cpuThreadNum != profile.cpuThreadNum) {
|
||||
predictor.releaseModel()
|
||||
predictor.cpuThreadNum = profile.cpuThreadNum
|
||||
}
|
||||
|
||||
// Apply per-variant model file names (both v3/v5).
|
||||
// zh-CN: 应用按变体区分的模型文件名 (同时覆盖 v3/v5).
|
||||
predictor.detModelFilename = profile.detModelFile
|
||||
predictor.recModelFilename = profile.recModelFile
|
||||
predictor.clsModelFilename = profile.clsModelFile
|
||||
|
||||
return if (profile.variantName == VariantSpec.NAME_V5) {
|
||||
// Use v5 built-in OpenCLGuard + fallback logic.
|
||||
// zh-CN: 使用 v5 内置的 OpenCLGuard + fallback 逻辑.
|
||||
predictor.init(
|
||||
context.applicationContext,
|
||||
profile.useSlim,
|
||||
profile.useOpenCL,
|
||||
)
|
||||
} else {
|
||||
// v3: OpenCL is ignored at variant level; init by resolved modelDir/labelPath.
|
||||
// zh-CN: v3: OpenCL 在变体层被忽略; 按解析后的 modelDir/labelPath 初始化.
|
||||
predictor.init(
|
||||
context.applicationContext,
|
||||
requireNotNull(profile.modelDir) {
|
||||
context.getString(R.string.error_missing_modeldir_for_variant, profile.variantName)
|
||||
},
|
||||
profile.labelAssetPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun recognizeText(bitmap: Bitmap, options: OcrOptions): List<String> {
|
||||
val results = predictor.runOcr(bitmap)
|
||||
val out = ArrayList<String>(results.size)
|
||||
for (r in results) out.add(r.label)
|
||||
|
||||
// Only print summary or first several items to avoid heavy I/O.
|
||||
// zh-CN: 仅打印摘要或前若干条, 避免大量 I/O.
|
||||
Log.i("PaddleOcrEngine", "recognized ${out.size} items")
|
||||
for (i in 0 until minOf(5, out.size)) {
|
||||
Log.d("PaddleOcrEngine", "item[$i]: ${out[i]}")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
override fun detect(bitmap: Bitmap, options: OcrOptions): List<OcrResult> {
|
||||
val results = predictor.runOcr(bitmap)
|
||||
return results.map { r ->
|
||||
OcrResult().apply {
|
||||
text = r.label
|
||||
confidence = r.confidence
|
||||
bounds = r.bounds
|
||||
extras = Bundle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.baidu.paddle.lite.ocr;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.media.ExifInterface;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* @author PaddleOCR
|
||||
* @since Aug 6, 2023
|
||||
*
|
||||
* <p> Modified by TonyJiangWJ as of Aug 7, 2023. </p>
|
||||
* <p> Modified by SuperMonster003 as of Jan 18, 2026. </p>
|
||||
*/
|
||||
@SuppressWarnings({"ResultOfMethodCallIgnored", "CallToPrintStackTrace", "unused"})
|
||||
public class Utils {
|
||||
private static final String TAG = Utils.class.getSimpleName();
|
||||
|
||||
public static void copyFileFromAssets(Context appCtx, String srcPath, String dstPath) {
|
||||
if (srcPath.isEmpty() || dstPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try (InputStream is = new BufferedInputStream(appCtx.getAssets().open(srcPath)); OutputStream os = new BufferedOutputStream(new FileOutputStream(dstPath))) {
|
||||
try {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, length);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyDirectoryFromAssets(Context appCtx, String srcDir, String dstDir) {
|
||||
if (srcDir.isEmpty() || dstDir.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!new File(dstDir).exists()) {
|
||||
new File(dstDir).mkdirs();
|
||||
}
|
||||
String[] list = appCtx.getAssets().list(srcDir);
|
||||
if (list == null) {
|
||||
return;
|
||||
}
|
||||
for (String fileName : list) {
|
||||
String srcSubPath = srcDir + File.separator + fileName;
|
||||
String dstSubPath = dstDir + File.separator + fileName;
|
||||
if (new File(srcSubPath).isDirectory()) {
|
||||
copyDirectoryFromAssets(appCtx, srcSubPath, dstSubPath);
|
||||
} else {
|
||||
Log.d(TAG, "Copy asset file: " + srcSubPath + " -> " + dstSubPath);
|
||||
copyFileFromAssets(appCtx, srcSubPath, dstSubPath);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyDirectoryFromAssetsIfNeeded(Context appCtx, String srcDir, String dstDir) {
|
||||
if (srcDir.isEmpty() || dstDir.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!new File(dstDir).exists()) {
|
||||
new File(dstDir).mkdirs();
|
||||
}
|
||||
String[] list = appCtx.getAssets().list(srcDir);
|
||||
if (list == null) {
|
||||
return;
|
||||
}
|
||||
for (String fileName : list) {
|
||||
String srcSubPath = srcDir + File.separator + fileName;
|
||||
String dstSubPath = dstDir + File.separator + fileName;
|
||||
if (new File(srcSubPath).isDirectory()) {
|
||||
copyDirectoryFromAssetsIfNeeded(appCtx, srcSubPath, dstSubPath);
|
||||
} else {
|
||||
if (new File(dstSubPath).exists()) {
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "Copy asset file: " + srcSubPath + " -> " + dstSubPath);
|
||||
copyFileFromAssets(appCtx, srcSubPath, dstSubPath);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static float[] parseFloatsFromString(String string, String delimiter) {
|
||||
String[] pieces = string.trim().toLowerCase().split(delimiter);
|
||||
float[] floats = new float[pieces.length];
|
||||
for (int i = 0; i < pieces.length; i++) {
|
||||
floats[i] = Float.parseFloat(pieces[i].trim());
|
||||
}
|
||||
return floats;
|
||||
}
|
||||
|
||||
public static long[] parseLongsFromString(String string, String delimiter) {
|
||||
String[] pieces = string.trim().toLowerCase().split(delimiter);
|
||||
long[] longs = new long[pieces.length];
|
||||
for (int i = 0; i < pieces.length; i++) {
|
||||
longs[i] = Long.parseLong(pieces[i].trim());
|
||||
}
|
||||
return longs;
|
||||
}
|
||||
|
||||
public static String getSDCardDirectory() {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath();
|
||||
}
|
||||
|
||||
public static boolean isSupportedNPU() {
|
||||
return false;
|
||||
// String hardware = android.os.Build.HARDWARE;
|
||||
// return hardware.equalsIgnoreCase("kirin810") || hardware.equalsIgnoreCase("kirin990");
|
||||
}
|
||||
|
||||
public static Bitmap resizeWithStep(Bitmap bitmap, int maxLength, int step) {
|
||||
int width = bitmap.getWidth();
|
||||
int height = bitmap.getHeight();
|
||||
int maxWH = Math.max(width, height);
|
||||
float ratio;
|
||||
int newWidth = width;
|
||||
int newHeight = height;
|
||||
if (maxWH > maxLength) {
|
||||
ratio = maxLength * 1.0f / maxWH;
|
||||
newWidth = (int) Math.floor(ratio * width);
|
||||
newHeight = (int) Math.floor(ratio * height);
|
||||
}
|
||||
|
||||
newWidth = newWidth - newWidth % step;
|
||||
if (newWidth == 0) {
|
||||
newWidth = step;
|
||||
}
|
||||
newHeight = newHeight - newHeight % step;
|
||||
if (newHeight == 0) {
|
||||
newHeight = step;
|
||||
}
|
||||
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
|
||||
}
|
||||
|
||||
public static Bitmap rotateBitmap(Bitmap bitmap, int orientation) {
|
||||
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_NORMAL:
|
||||
return bitmap;
|
||||
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
|
||||
matrix.setScale(-1, 1);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
matrix.setRotate(180);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
|
||||
matrix.setRotate(180);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_TRANSPOSE:
|
||||
matrix.setRotate(90);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
matrix.setRotate(90);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_TRANSVERSE:
|
||||
matrix.setRotate(-90);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
matrix.setRotate(-90);
|
||||
break;
|
||||
default:
|
||||
return bitmap;
|
||||
}
|
||||
try {
|
||||
Bitmap bmRotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
||||
bitmap.recycle();
|
||||
return bmRotated;
|
||||
} catch (OutOfMemoryError e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">تعذّرت تهيئة Paddle OCR predictor.</string>
|
||||
<string name="error_missing_modeldir_for_variant">modelDir مفقود لـ variant=%1$s.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">تعذّر فك ترميز مورد drawable الخاص بالتحقق.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">مورد Paddle OCR المطلوب مفقود: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">Failed to initialize Paddle OCR predictor.</string>
|
||||
<string name="error_missing_modeldir_for_variant">Missing modelDir for variant=%1$s.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">Failed to decode checking drawable resource.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">Missing required Paddle OCR asset: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">No se pudo inicializar el predictor de Paddle OCR.</string>
|
||||
<string name="error_missing_modeldir_for_variant">Falta modelDir para variant=%1$s.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">No se pudo decodificar el recurso drawable de verificación.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">Falta el recurso requerido de Paddle OCR: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">Échec de l\'initialisation du prédicteur Paddle OCR.</string>
|
||||
<string name="error_missing_modeldir_for_variant">modelDir manquant pour variant=%1$s.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">Échec du décodage de la ressource drawable de vérification.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">Ressource Paddle OCR requise manquante: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">Paddle OCR predictor の初期化に失敗しました.</string>
|
||||
<string name="error_missing_modeldir_for_variant">variant=%1$s の modelDir が見つかりません.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">確認用 drawable リソースのデコードに失敗しました.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">必要な Paddle OCR asset が見つかりません: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">Paddle OCR predictor 초기화에 실패했습니다.</string>
|
||||
<string name="error_missing_modeldir_for_variant">variant=%1$s에 대한 modelDir가 없습니다.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">검사용 drawable 리소스 디코딩에 실패했습니다.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">필수 Paddle OCR asset이 없습니다: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">Не удалось инициализировать predictor Paddle OCR.</string>
|
||||
<string name="error_missing_modeldir_for_variant">Отсутствует modelDir для variant=%1$s.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">Не удалось декодировать проверочный ресурс drawable.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">Отсутствует требуемый ресурс Paddle OCR: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">初始化 Paddle OCR predictor 失敗.</string>
|
||||
<string name="error_missing_modeldir_for_variant">變體缺少 modelDir: variant=%1$s.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">解碼檢查用 drawable 資源失敗.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">缺少 Paddle OCR 必要資源文件: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">初始化 Paddle OCR predictor 失敗.</string>
|
||||
<string name="error_missing_modeldir_for_variant">變體缺少 modelDir: variant=%1$s.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">解碼檢查用 drawable 資源失敗.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">缺少 Paddle OCR 必要資原始檔: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">初始化 Paddle OCR predictor 失败.</string>
|
||||
<string name="error_missing_modeldir_for_variant">变体缺少 modelDir: variant=%1$s.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">解码检查用 drawable 资源失败.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">缺少 Paddle OCR 必要资源文件: %1$s.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_failed_to_initialize_paddle_ocr_predictor">Failed to initialize Paddle OCR predictor.</string>
|
||||
<string name="error_missing_modeldir_for_variant">Missing modelDir for variant=%1$s.</string>
|
||||
<string name="error_failed_to_decode_checking_drawable_resource">Failed to decode checking drawable resource.</string>
|
||||
<string name="error_missing_required_paddle_ocr_asset">Missing required Paddle OCR asset: %1$s.</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user