/* * 自然写互动课堂应用开发SDK软件 V1.0 * OCREngine - OCR识别引擎封装 * * 功能说明: * 1. 本地离线OCR识别(ONNX Runtime推理) * 2. 云端在线OCR识别(REST API调用AI引擎) * 3. 识别结果缓存与去重 * 4. 批量识别任务队列 * 5. 识别模式自动切换(在线优先,离线兜底) */ package com.writech.sdk.android; import android.content.Context; import android.graphics.Bitmap; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; /** * OCR识别引擎 * 封装本地ONNX推理与云端AI引擎调用 */ public class OCREngine { private static final String TAG = "WritechOCREngine"; /* 识别模式枚举 */ public static final int MODE_AUTO = 0; /* 自动(在线优先,离线兜底) */ public static final int MODE_ONLINE_ONLY = 1; /* 仅在线 */ public static final int MODE_OFFLINE_ONLY = 2; /* 仅离线 */ /* 识别类型枚举 */ public static final int TYPE_HANDWRITING = 0; /* 手写文字识别 */ public static final int TYPE_MATH = 1; /* 数学公式识别 */ public static final int TYPE_STROKE_ORDER = 2; /* 笔顺评分 */ /* 云端API超时时间(毫秒) */ private static final int API_TIMEOUT_MS = 5000; /* 最大离线缓存条目数 */ private static final int MAX_CACHE_SIZE = 500; /* ========== 成员变量 ========== */ private final Context mContext; private int mRecognitionMode = MODE_AUTO; /* 离线ONNX模型文件路径 */ private String mOnnxModelPath; private boolean mOfflineModelLoaded = false; /* ONNX推理会话句柄(通过JNI调用C层) */ private long mOnnxSessionHandle = 0; /* 云端API基础地址 */ private String mCloudApiBaseUrl; private String mApiAccessToken; /* 识别任务队列 */ private final Queue mTaskQueue = new ConcurrentLinkedQueue<>(); private final AtomicBoolean mIsProcessing = new AtomicBoolean(false); /* 后台处理线程 */ private HandlerThread mWorkerThread; private Handler mWorkerHandler; /* 结果缓存(简单LRU) */ private final LinkedList mResultCache = new LinkedList<>(); /* ========== 内部数据结构 ========== */ /** 识别任务 */ private static class RecognitionTask { int taskId; /* 任务ID */ int recognitionType; /* 识别类型 */ Bitmap inputImage; /* 输入图像 */ byte[] strokeData; /* 笔迹数据(笔顺识别用) */ String targetChar; /* 目标汉字(笔顺识别用) */ RecognitionCallback callback; /* 结果回调 */ } /** 缓存条目 */ private static class CacheEntry { String cacheKey; /* 缓存键(图像哈希) */ String result; /* 识别结果 */ long timestamp; /* 缓存时间 */ } /** 识别结果回调接口 */ public interface RecognitionCallback { void onSuccess(String result, float confidence, boolean fromCache); void onError(int errorCode, String errorMessage); } /* ========== 构造与初始化 ========== */ /** * 创建OCR引擎实例 * @param context Android上下文 * @param cloudBaseUrl 云端AI引擎API地址 * @param accessToken API访问令牌 */ public OCREngine(Context context, String cloudBaseUrl, String accessToken) { mContext = context.getApplicationContext(); mCloudApiBaseUrl = cloudBaseUrl; mApiAccessToken = accessToken; /* 创建后台处理线程 */ mWorkerThread = new HandlerThread("WritechOCR"); mWorkerThread.start(); mWorkerHandler = new Handler(mWorkerThread.getLooper()); Log.i(TAG, "OCR引擎初始化完成,云端地址: " + cloudBaseUrl); } /** * 加载离线ONNX识别模型 * 从assets或本地文件加载预训练的手写识别模型 * * @param modelPath 模型文件路径(.onnx格式) * @return 是否加载成功 */ public boolean loadOfflineModel(String modelPath) { File modelFile = new File(modelPath); if (!modelFile.exists()) { Log.e(TAG, "离线模型文件不存在: " + modelPath); return false; } /* 通过JNI调用C层ONNX Runtime加载模型 */ mOnnxSessionHandle = nativeLoadModel(modelPath); if (mOnnxSessionHandle != 0) { mOnnxModelPath = modelPath; mOfflineModelLoaded = true; Log.i(TAG, "离线ONNX模型加载成功: " + modelPath); return true; } Log.e(TAG, "离线ONNX模型加载失败"); return false; } /** 设置识别模式 */ public void setRecognitionMode(int mode) { mRecognitionMode = mode; } /* ========== 识别请求接口 ========== */ /** * 提交手写文字识别任务 * @param image 笔迹图像(已渲染的Bitmap) * @param callback 结果回调 * @return 任务ID */ public int recognizeHandwriting(Bitmap image, RecognitionCallback callback) { return submitTask(TYPE_HANDWRITING, image, null, null, callback); } /** * 提交数学公式识别任务 * @param image 公式图像 * @param callback 结果回调 * @return 任务ID */ public int recognizeMath(Bitmap image, RecognitionCallback callback) { return submitTask(TYPE_MATH, image, null, null, callback); } /** * 提交笔顺评分任务 * @param strokeData 笔迹轨迹数据(序列化的坐标数组) * @param targetChar 目标汉字 * @param callback 结果回调 * @return 任务ID */ public int evaluateStrokeOrder(byte[] strokeData, String targetChar, RecognitionCallback callback) { return submitTask(TYPE_STROKE_ORDER, null, strokeData, targetChar, callback); } /* ========== 任务管理 ========== */ private int mTaskIdCounter = 0; /** 提交识别任务到队列 */ private int submitTask(int type, Bitmap image, byte[] strokeData, String targetChar, RecognitionCallback callback) { RecognitionTask task = new RecognitionTask(); task.taskId = ++mTaskIdCounter; task.recognitionType = type; task.inputImage = image; task.strokeData = strokeData; task.targetChar = targetChar; task.callback = callback; mTaskQueue.offer(task); Log.d(TAG, "识别任务已提交 #" + task.taskId + " 类型=" + type); /* 如果没有正在处理的任务,启动处理循环 */ if (mIsProcessing.compareAndSet(false, true)) { mWorkerHandler.post(this::processNextTask); } return task.taskId; } /** 处理队列中的下一个任务 */ private void processNextTask() { RecognitionTask task = mTaskQueue.poll(); if (task == null) { mIsProcessing.set(false); return; } Log.d(TAG, "开始处理识别任务 #" + task.taskId); try { /* 检查缓存 */ String cacheKey = computeCacheKey(task); String cachedResult = lookupCache(cacheKey); if (cachedResult != null) { task.callback.onSuccess(cachedResult, 1.0f, true); Log.d(TAG, "任务 #" + task.taskId + " 命中缓存"); mWorkerHandler.post(this::processNextTask); return; } String result = null; float confidence = 0.0f; /* 根据识别模式选择执行路径 */ switch (mRecognitionMode) { case MODE_ONLINE_ONLY: result = executeCloudRecognition(task); confidence = 0.95f; break; case MODE_OFFLINE_ONLY: result = executeOfflineRecognition(task); confidence = 0.85f; break; case MODE_AUTO: default: /* 自动模式:先尝试在线,失败则回退到离线 */ try { result = executeCloudRecognition(task); confidence = 0.95f; } catch (Exception e) { Log.w(TAG, "在线识别失败,回退到离线: " + e.getMessage()); result = executeOfflineRecognition(task); confidence = 0.85f; } break; } if (result != null) { /* 存入缓存 */ putCache(cacheKey, result); task.callback.onSuccess(result, confidence, false); } else { task.callback.onError(-1, "识别失败,无可用结果"); } } catch (Exception e) { Log.e(TAG, "识别任务 #" + task.taskId + " 异常: " + e.getMessage()); task.callback.onError(-2, e.getMessage()); } /* 继续处理下一个任务 */ mWorkerHandler.post(this::processNextTask); } /* ========== 云端识别 ========== */ /** 调用云端AI引擎执行识别 */ private String executeCloudRecognition(RecognitionTask task) throws IOException { String apiPath; switch (task.recognitionType) { case TYPE_MATH: apiPath = "/api/v1/math/recognize"; break; case TYPE_STROKE_ORDER: apiPath = "/api/v1/stroke-order/evaluate"; break; case TYPE_HANDWRITING: default: apiPath = "/api/v1/ocr/recognize"; break; } String url = mCloudApiBaseUrl + apiPath; Log.d(TAG, "调用云端识别API: " + url); /* 构建multipart请求体 */ byte[] imageBytes = null; if (task.inputImage != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); task.inputImage.compress(Bitmap.CompressFormat.PNG, 100, baos); imageBytes = baos.toByteArray(); } /* 使用CloudClient发送HTTP请求 */ String responseJson = CloudClient.postMultipart(url, mApiAccessToken, imageBytes, task.strokeData, task.targetChar, API_TIMEOUT_MS); /* 解析JSON响应提取识别结果 */ return parseRecognitionResult(responseJson); } /* ========== 离线识别 ========== */ /** 使用本地ONNX模型执行离线识别 */ private String executeOfflineRecognition(RecognitionTask task) { if (!mOfflineModelLoaded || mOnnxSessionHandle == 0) { Log.e(TAG, "离线模型未加载"); return null; } if (task.inputImage == null) { Log.e(TAG, "离线识别需要输入图像"); return null; } /* 图像预处理:缩放到模型输入尺寸,转为灰度float数组 */ float[] inputTensor = preprocessImage(task.inputImage); /* 通过JNI调用ONNX Runtime执行推理 */ String result = nativeRunInference(mOnnxSessionHandle, inputTensor, task.inputImage.getWidth(), task.inputImage.getHeight()); return result; } /** 图像预处理(缩放+归一化) */ private float[] preprocessImage(Bitmap bitmap) { int targetWidth = 320; int targetHeight = 48; /* 保持宽高比缩放 */ float scale = Math.min( (float) targetWidth / bitmap.getWidth(), (float) targetHeight / bitmap.getHeight() ); int scaledW = (int) (bitmap.getWidth() * scale); int scaledH = (int) (bitmap.getHeight() * scale); Bitmap scaled = Bitmap.createScaledBitmap(bitmap, scaledW, scaledH, true); float[] tensor = new float[targetWidth * targetHeight]; /* 填充灰度值并归一化到[0, 1] */ for (int y = 0; y < scaledH && y < targetHeight; y++) { for (int x = 0; x < scaledW && x < targetWidth; x++) { int pixel = scaled.getPixel(x, y); /* 灰度化:0.299R + 0.587G + 0.114B */ float gray = (0.299f * ((pixel >> 16) & 0xFF) + 0.587f * ((pixel >> 8) & 0xFF) + 0.114f * (pixel & 0xFF)) / 255.0f; tensor[y * targetWidth + x] = gray; } } scaled.recycle(); return tensor; } /* ========== 结果缓存 ========== */ /** 计算缓存键 */ private String computeCacheKey(RecognitionTask task) { if (task.inputImage != null) { return "img_" + task.recognitionType + "_" + task.inputImage.hashCode(); } if (task.strokeData != null && task.targetChar != null) { return "stroke_" + task.targetChar + "_" + task.strokeData.length; } return "unknown_" + task.taskId; } /** 查找缓存 */ private String lookupCache(String key) { synchronized (mResultCache) { for (CacheEntry entry : mResultCache) { if (entry.cacheKey.equals(key)) { /* 检查过期(5分钟) */ if (System.currentTimeMillis() - entry.timestamp < 300000) { return entry.result; } } } } return null; } /** 存入缓存 */ private void putCache(String key, String result) { synchronized (mResultCache) { CacheEntry entry = new CacheEntry(); entry.cacheKey = key; entry.result = result; entry.timestamp = System.currentTimeMillis(); mResultCache.addFirst(entry); /* 限制缓存大小 */ while (mResultCache.size() > MAX_CACHE_SIZE) { mResultCache.removeLast(); } } } /** 解析云端识别API返回的JSON */ private String parseRecognitionResult(String json) { if (json == null || json.isEmpty()) return null; /* 简化的JSON解析:提取result字段 */ int idx = json.indexOf("\"result\""); if (idx < 0) return null; int start = json.indexOf("\"", idx + 8) + 1; int end = json.indexOf("\"", start); if (start > 0 && end > start) { return json.substring(start, end); } return null; } /* ========== JNI本地方法声明 ========== */ /** 加载ONNX模型,返回会话句柄 */ private native long nativeLoadModel(String modelPath); /** 执行ONNX推理,返回识别结果JSON */ private native String nativeRunInference(long sessionHandle, float[] inputTensor, int width, int height); /** 释放ONNX会话资源 */ private native void nativeReleaseModel(long sessionHandle); static { System.loadLibrary("writech_ocr"); } /* ========== 资源释放 ========== */ /** 释放OCR引擎资源 */ public void destroy() { mTaskQueue.clear(); if (mOnnxSessionHandle != 0) { nativeReleaseModel(mOnnxSessionHandle); mOnnxSessionHandle = 0; } if (mWorkerThread != null) { mWorkerThread.quitSafely(); mWorkerThread = null; } mResultCache.clear(); Log.i(TAG, "OCR引擎资源已释放"); } }