更新记录
1.0.0(2026-09-16)
- 支持本地图片识别和实时扫描。
- 支持普通文本及常用证件、车牌、银行卡、发票等结构化字段提取。
平台兼容性
uni-app(5.23)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | × | × | √ | √ | 8.0 | × | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
uni-app x(5.23)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| × | × | 8.0 | × | × | × |
yt-offlineocr
一款离线OCR插件,可识别本地图片,相机拍照识别,也可以打开实时扫描页面完成识别。运行期间无需请求网络接口,待识别图片和识别结果均保留在设备本地。支持身份证、银行卡、社保卡、车牌号、车架号、发票等
特别提醒
- 购买本插件前,请先试用,请先试用,请先试用,确认满足需求之后再行购买。虚拟物品一旦购买之后无法退款。
- 如有使用上的疑问、bug,可以进交流群联系作者;
- 请在合法范围内使用,若使用本插件做非法开发,本方概不负责;
- uniapp-x项目调用api需要加上 as 类型转换
平台要求
- 仅支持 Android。
- 最低支持 Android 8.0(API 26)。
- 支持
armeabi-v7a、arm64-v8a设备。 - 适配16kb
- 实时扫描需要相机权限。
- 使用原生能力,调试前需要重新制作包含本插件的自定义调试基座;修改插件后也需要重新制作基座。
支持的识别类型
mode |
说明 | 实时扫描行为 |
|---|---|---|
TEXT |
普通文本 | 显示识别内容,由用户点击确定返回 |
ID_CARD_FRONT |
身份证正面 | 匹配成功后自动返回 |
ID_CARD_BACK |
身份证反面 | 匹配成功后自动返回 |
LICENSE_PLATE |
机动车车牌 | 匹配成功后自动返回 |
BANK_CARD |
银行卡 | 匹配成功后自动返回 |
BUSINESS_LICENSE |
营业执照 | 匹配成功后自动返回 |
SOCIAL_SECURITY_CARD |
社会保障卡 | 匹配成功后自动返回 |
INVOICE |
发票 | 匹配成功后自动返回 |
VIN |
VIN/车架号 | 匹配成功后自动返回 |
VEHICLE_LICENSE |
行驶证 | 匹配成功后自动返回 |
PHONE_NUMBER |
手机号码 | 匹配成功后自动返回 |
BICYCLE_PLATE |
自行车牌照 | 匹配成功后自动返回 |
结构化类型只有在识别内容符合所选类型时才会自动返回。若选择了身份证正面,但取景框中不是身份证正面,扫描页面会继续识别,不会触发成功回调。
集成步骤
1. 放置插件
确认项目中存在以下目录:
uni_modules/yt-offlineocr
导入插件 API 时直接使用 uni_modules 路径,无需在 main.js 中注册。
2. 配置 Android 权限
在项目 manifest.json 的 app-plus.distribute.android.permissions 中加入:
"permissions": [
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" android:maxSdkVersion=\"32\"/>"
]
权限用途:
CAMERA:打开实时扫描页面。插件默认会在调用时检查并申请运行时权限。READ_MEDIA_IMAGES:Android 13 及以上访问用户选择的相册图片。READ_EXTERNAL_STORAGE:Android 12 及以下访问用户选择的相册图片。
如果项目原有 permissions 数组中已经存在同名权限,不要重复添加。
3. 导入 API
import {
initializeOfflineOcr,
recognizeOfflineOcr,
startOfflineOcrScan,
releaseOfflineOcr
} from '@/uni_modules/yt-offlineocr'
插件提供以下四个方法:
initializeOfflineOcr(options):提前初始化识别资源,可选调用。recognizeOfflineOcr(options):识别一张本地图片。startOfflineOcrScan(options):打开实时扫描页面。releaseOfflineOcr(options):释放图片识别占用的资源。
提前初始化
初始化不是必需步骤。如果没有主动调用,第一次执行图片识别时会自动初始化。对首次识别响应速度有要求时,可以在进入业务页面后提前调用。
initializeOfflineOcr({
cpuThreadCount: 4,
detectionThreshold: 0.3,
detectionBoxThreshold: 0.6,
recognitionBatchSize: 1,
maxImageDimension: 2560,
success: result => {
console.log('初始化成功', result)
},
fail: error => {
console.log('初始化失败', error.errCode, error.errMsg)
},
complete: result => {
console.log('初始化调用结束', result)
}
})
初始化参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
cpuThreadCount |
number |
否 | 4 |
识别使用的 CPU 线程数,允许范围 1~8 |
detectionThreshold |
number |
否 | 0.3 |
文本检测阈值,允许范围 0~1 |
detectionBoxThreshold |
number |
否 | 0.6 |
文本区域过滤阈值,允许范围 0~1 |
recognitionBatchSize |
number |
否 | 1 |
单次识别批量大小,最小值为 1 |
maxImageDimension |
number |
否 | 2560 |
输入图片最大边长,最小值为 640 |
success |
Function |
否 | - | 初始化成功回调 |
fail |
Function |
否 | - | 初始化失败回调 |
complete |
Function |
否 | - | 成功或失败后都会执行 |
一般业务建议先使用默认值。只有经过不同设备和图片场景测试后,再调整阈值或尺寸。
初始化成功结果
| 字段 | 类型 | 说明 |
|---|---|---|
success |
boolean |
是否初始化成功 |
modelLoadMs |
number |
初始化耗时,单位毫秒 |
message |
string |
结果说明 |
识别相册图片
可以直接使用 uni.chooseImage 返回的临时图片路径:
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: chooseResult => {
const imagePath = chooseResult.tempFilePaths[0]
recognizeOfflineOcr({
imagePath,
mode: 'ID_CARD_FRONT',
minConfidence: 0.35,
success: result => {
console.log('完整文字', result.text)
console.log('逐行结果', result.lines)
console.log('结构化字段', result.fields)
},
fail: error => {
console.log('识别失败', error.errCode, error.errMsg)
},
complete: result => {
console.log('图片识别结束', result)
}
})
},
fail: error => {
console.log('没有选择图片', error)
}
})
图片识别参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
imagePath |
string |
是 | - | 本地图片路径,可使用 uni.chooseImage 返回的路径、绝对路径、file:// 或 content:// |
mode |
OfflineOcrMode |
否 | TEXT |
识别类型 |
minConfidence |
number |
否 | 0.35 |
最低置信度,允许范围 0~1 |
success |
Function |
否 | - | 识别成功回调 |
fail |
Function |
否 | - | 识别失败回调 |
complete |
Function |
否 | - | 成功或失败后都会执行 |
实时扫描
startOfflineOcrScan({
mode: 'ID_CARD_FRONT',
minConfidence: 0.35,
hint: '',
stableFrameCount: 2,
requestCameraPermission: true,
success: result => {
console.log('扫描成功', result)
},
fail: error => {
if (error.errCode === -31003) {
console.log('用户取消了扫描')
return
}
console.log('扫描失败', error.errCode, error.errMsg)
},
complete: result => {
console.log('实时扫描结束', result)
}
})
实时扫描参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
mode |
OfflineOcrMode |
否 | TEXT |
识别类型 |
minConfidence |
number |
否 | 0.35 |
最低置信度,允许范围 0~1 |
hint |
string |
否 | '' |
取景框下方提示;留空时根据识别类型显示默认提示 |
stableFrameCount |
number |
否 | 2 |
结构化结果连续稳定多少帧后自动返回,最小值为 1 |
requestCameraPermission |
boolean |
否 | true |
是否由插件检查并申请相机权限 |
success |
Function |
否 | - | 扫描成功回调 |
fail |
Function |
否 | - | 取消或扫描失败回调 |
complete |
Function |
否 | - | 成功、取消或失败后都会执行 |
建议保持 requestCameraPermission: true。只有业务页面已经完成相机权限申请并确认获得授权时,才设置为 false。
扫描页面行为
- 点击取景框区域可以重新聚焦。
- 支持在扫描页打开或关闭补光灯。
- 身份证正面、身份证反面会显示对应的取景提示。
TEXT模式显示识别内容和确定按钮,由用户确认后返回。- 除
TEXT外的结构化类型不显示底部识别内容和确定按钮,匹配成功并达到稳定帧数后自动返回。 - 点击关闭按钮或系统返回键时进入
fail回调,错误码为-31003。
识别成功结果
图片识别和实时扫描使用相同的成功结果结构:
| 字段 | 类型 | 说明 |
|---|---|---|
status |
string |
结果状态 |
mode |
string |
本次使用的识别类型 |
text |
string |
按行合并后的完整文字 |
lines |
Array<OfflineOcrLine> |
逐行文字、置信度和位置 |
fields |
Array<OfflineOcrField> |
当前识别类型提取出的结构化字段 |
warnings |
Array<string> |
字段缺失、格式校验等提示;没有提示时为空数组 |
timing |
OfflineOcrTiming |
各阶段耗时,单位毫秒 |
rawJson |
string |
完整结果 JSON,可用于日志、存储或透传 |
lines 每项字段
| 字段 | 类型 | 说明 |
|---|---|---|
text |
string |
当前行文字 |
confidence |
number |
当前行置信度 |
points |
Array<{x, y}> |
当前文字区域的顶点坐标 |
fields 每项字段
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string |
字段名称 |
value |
string |
字段值 |
不同识别类型返回的字段名称和数量不同。业务代码应根据 name 查找字段,不要依赖数组固定下标。
释放资源
建议在识别业务页面销毁时调用:
export default {
onUnload() {
releaseOfflineOcr({
success: result => {
console.log(result.message)
}
})
}
}
释放后仍可以继续调用初始化或图片识别,插件会重新准备所需资源。
错误结果
fail 回调收到的对象结构:
| 字段 | 类型 | 说明 |
|---|---|---|
errCode |
number |
插件统一错误码,建议业务主要判断此字段 |
errMsg |
string |
可读错误说明 |
code |
string |
扩展错误标识 |
cause |
string \| null |
更详细的错误原因,可能为空 |
错误码
errCode |
说明 | 建议处理 |
|---|---|---|
-31001 |
当前页面不可用于启动原生功能 | 确认应用位于前台后重试 |
-31002 |
相机权限被拒绝 | 提示用户授权;永久拒绝时引导到系统设置 |
-31003 |
用户取消实时扫描 | 通常无需提示错误 |
-31004 |
已有实时扫描正在进行 | 等待本次扫描结束后再调用 |
-31005 |
图片路径为空 | 检查 imagePath |
-31006 |
识别结果解析失败 | 记录错误信息并重试 |
-31101 |
初始化失败 | 释放后重试,并检查设备可用内存 |
-31102 |
图片识别失败 | 检查图片路径、格式和文件可读性 |
使用注意事项
- 插件方法均为异步回调形式,不要使用同步返回值接收识别结果。
- 第一次识别准备时间通常比后续识别长,可在业务空闲时提前初始化。
- 同一时间只允许打开一个实时扫描页面;请防止按钮连续点击。
- 结构化类型必须与实际证件或号码类型一致,否则实时扫描不会自动成功返回。
- 拍摄时应保证文字完整、光线均匀、画面清晰,尽量避免反光、阴影和大角度倾斜。
minConfidence越高,结果过滤越严格;不确定时建议保留默认值。- 大图片会增加识别耗时和内存占用,通常无需主动提高
maxImageDimension。 - 页面销毁后如不再继续识别,应调用
releaseOfflineOcr。 - 修改插件文件或升级插件后,需要重新制作自定义调试基座,旧基座不会包含最新原生能力。
- 完整可运行示例位于项目
pages/index/index.vue。
更多好用实惠插件
- 高德定位连续定位后台定位保活定位
- 百度汽车摩托车导航插件
- 百度鹰眼轨迹插件支持后台采集、保活
- 百度定位插件、连续定位、保活、坐标系转换、支持双端
- 计步器插件,支持Android、iOS双端
- uts经典蓝牙插件、蓝牙电子秤
- 获取唯一标识、ServiceID、卸载更新不变iOS+Android
- Android经典蓝牙
- 华为ScanKit统一扫码插件支持iOS+Android原生插件
- 【华为扫码】统一扫码插件支持多码连续扫码支持半屏扫码uts插件iOS+Android+HarmonyOS
- 截屏、录屏、防截屏、录屏iOS、Android
- 人脸采集插件 最新百度SDK 离线人脸采集、活体检测
- 页面截长图、截取WebView内容,生成长截图Android+iOS
- Android无预览拍照、录制、静默拍照、静默录制、抓拍插件支持
- uni高德地图功能拓展地图截图
- 科大讯飞离线合成插件支持iOS、android
- iOS保活Android保活鸿蒙保活定位插件系统定位
- 高德定位、猎鹰轨迹插件
- 海康威视综合安防平台视频播放插件
- 支持NFC读写功能检测支持Android iOS HarmonyOS
- 自定义相机
- VLC视频播放器
- ijkPlayer万能视频播放器支持Android iOS 鸿蒙
uniapp完整示例
<template>
<scroll-view class="page" scroll-y>
<view class="header">
<text class="title">离线 OCR</text>
<text class="subtitle">yt-offlineocr · UTS插件 调用示例</text>
</view>
<view class="card tip-card">
<text class="card-title">功能说明</text>
<text class="tip">模型、图片和识别结果全部保留在设备本地,不依赖网络接口。</text>
<text class="tip">普通文本实时扫描需要手动确认;身份证、银行卡、车牌等结构化类型匹配成功后自动返回。</text>
</view>
<view class="card">
<text class="card-title">1. 选择识别类型</text>
<picker :range="modeLabels" :value="selectedModeIndex" @change="onModeChange">
<view class="picker-value">{{ selectedModeLabel }} ›</view>
</picker>
<text class="mode-code">原生模式:{{ selectedMode }}</text>
</view>
<view class="card">
<text class="card-title">2. 初始化与图片识别</text>
<text class="tip">初始化不是必需步骤;不调用时,第一次图片识别会使用默认参数自动加载模型。</text>
<button type="primary" :disabled="busy" @click="onInitialize">提前初始化离线模型</button>
<button class="secondary-button" :disabled="busy" @click="onChooseImage">选择图片并识别</button>
<image v-if="imagePath" class="preview-image" :src="imagePath" mode="aspectFit" />
<text v-if="imagePath" class="path-text">图片:{{ imagePath }}</text>
</view>
<view class="card">
<text class="card-title">3. 实时扫描</text>
<text class="tip">插件会按需申请 CAMERA 权限,然后打开 AAR 内置的 CameraX 扫描页面。</text>
<button type="primary" :disabled="busy" @click="onStartScan">打开实时扫描</button>
</view>
<view class="card result-card">
<view class="result-header">
<text class="card-title">调用结果</text>
<button class="mini-button" size="mini" :disabled="busy" @click="onRelease">释放模型</button>
</view>
<text class="status">{{ statusText }}</text>
<template v-if="lastResult">
<text class="section-title">完整文字</text>
<text class="result-text">{{ lastResult.text || '未识别到文字' }}</text>
<template v-if="lastResult.fields && lastResult.fields.length">
<text class="section-title">结构化字段</text>
<view v-for="(field, index) in lastResult.fields" :key="index" class="field-row">
<text class="field-name">{{ field.name }}</text>
<text class="field-value">{{ field.value }}</text>
</view>
</template>
<text class="section-title">耗时</text>
<text class="timing-text">检测 {{ lastResult.timing.detectionMs }}ms;识别 {{ lastResult.timing.recognitionMs }}ms;总计 {{ lastResult.timing.totalMs }}ms</text>
</template>
<text class="section-title">完整回调对象</text>
<text class="json-text" selectable>{{ resultJson }}</text>
</view>
<view class="bottom-space"></view>
</scroll-view>
</template>
<script>
import {
initializeOfflineOcr,
recognizeOfflineOcr,
startOfflineOcrScan,
releaseOfflineOcr
} from '@/uni_modules/yt-offlineocr'
export default {
data() {
return {
/** 页面是否正在初始化、识别或等待扫描结果,用于防止重复点击。 */
busy: false,
/** uni.chooseImage 返回的本地临时路径,直接传给 UTS 插件。 */
imagePath: '',
/** 最近一次成功的 OfflineOcrResult,模板用它展示文字和字段。 */
lastResult: null,
/** success/fail/complete 的调试输出。 */
resultJson: '尚未调用',
statusText: '请选择识别类型,然后初始化、选择图片或打开实时扫描。',
selectedModeIndex: 0,
/**
* value 必须与原生 OcrMode 枚举一致;label 仅用于页面展示。
* 若业务只需要少数场景,可以删除不使用的选项。
*/
modeOptions: [
{ label: '普通文本', value: 'TEXT' },
{ label: '身份证正面', value: 'ID_CARD_FRONT' },
{ label: '身份证反面', value: 'ID_CARD_BACK' },
{ label: '车牌', value: 'LICENSE_PLATE' },
{ label: '银行卡', value: 'BANK_CARD' },
{ label: '营业执照', value: 'BUSINESS_LICENSE' },
{ label: '社会保障卡', value: 'SOCIAL_SECURITY_CARD' },
{ label: '发票', value: 'INVOICE' },
{ label: 'VIN 车架号', value: 'VIN' },
{ label: '行驶证', value: 'VEHICLE_LICENSE' },
{ label: '手机号码', value: 'PHONE_NUMBER' },
{ label: '自行车牌照', value: 'BICYCLE_PLATE' }
]
}
},
computed: {
modeLabels() {
return this.modeOptions.map(item => item.label)
},
selectedMode() {
return this.modeOptions[this.selectedModeIndex].value
},
selectedModeLabel() {
return this.modeOptions[this.selectedModeIndex].label
}
},
onUnload() {
// 页面销毁时释放图片识别复用的 ONNX Session,避免长期占用内存。
// 实时扫描 Activity 内部使用自己的 Client,退出扫描页时已自动释放。
releaseOfflineOcr({})
},
methods: {
/** picker 返回的是字符串索引,需要转成 Number 后保存。 */
onModeChange(event) {
this.selectedModeIndex = Number(event.detail.value)
this.lastResult = null
this.statusText = `已选择:${this.selectedModeLabel}`
},
/** 将任意回调对象格式化后同时写入状态区和控制台。 */
showCallback(callbackName, value) {
this.resultJson = JSON.stringify(value, null, 2)
console.log(`[yt-offlineocr] ${callbackName}: ${this.resultJson}`)
},
/**
* 提前加载 PP-OCRv6 检测、识别模型。
* cpuThreadCount 可按设备性能调整,常规 Android 手机推荐 2~4。
*/
onInitialize() {
this.busy = true
this.statusText = '正在初始化离线模型…'
initializeOfflineOcr({
cpuThreadCount: 4,
detectionThreshold: 0.3,
detectionBoxThreshold: 0.6,
recognitionBatchSize: 1,
maxImageDimension: 2560,
success: result => {
this.statusText = `模型初始化成功,加载耗时 ${result.modelLoadMs}ms`
this.showCallback('initialize success', result)
},
fail: error => {
this.statusText = `初始化失败:${error.errMsg}`
this.showCallback('initialize fail', error)
},
complete: () => {
this.busy = false
}
})
},
/** 从系统相册选择一张图片,再调用 AAR 的异步图片识别接口。 */
onChooseImage() {
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: chooseResult => {
const path = chooseResult.tempFilePaths[0]
this.imagePath = path
this.recognizeImage(path)
}
})
},
/**
* 识别指定本地图片。
* 结构化类型仍会返回 text/lines,同时尝试在 fields 中提取对应字段。
*/
recognizeImage(path) {
this.busy = true
this.lastResult = null
this.statusText = `正在识别${this.selectedModeLabel}…`
recognizeOfflineOcr({
imagePath: path,
mode: this.selectedMode,
minConfidence: 0.35,
success: result => {
this.lastResult = result
this.statusText = `识别完成,共 ${result.lines.length} 行,${result.fields.length} 个结构化字段。`
this.showCallback('recognize success', result)
},
fail: error => {
this.statusText = `图片识别失败:${error.errMsg}`
this.showCallback('recognize fail', error)
},
complete: () => {
this.busy = false
}
})
},
/**
* 打开 CameraX 实时扫描页。
* hint 传空字符串,让原生页面按照 mode 自动生成取景框提示。
*/
onStartScan() {
this.busy = true
this.lastResult = null
this.statusText = `正在打开${this.selectedModeLabel}实时扫描…`
startOfflineOcrScan({
mode: this.selectedMode,
minConfidence: 0.35,
hint: '',
stableFrameCount: 2,
requestCameraPermission: true,
success: result => {
this.lastResult = result
this.statusText = `实时扫描成功,返回 ${result.fields.length} 个结构化字段。`
this.showCallback('scan success', result)
},
fail: error => {
this.statusText = `实时扫描未完成:${error.errMsg}`
this.showCallback('scan fail', error)
},
complete: () => {
this.busy = false
}
})
},
/** 手动释放图片识别模型;释放后仍可再次初始化或识别。 */
onRelease() {
releaseOfflineOcr({
success: result => {
this.lastResult = null
this.statusText = result.message
this.showCallback('release success', result)
}
})
}
}
}
</script>
<style>
page {
background: #f3f5f8;
}
.page {
height: 100vh;
box-sizing: border-box;
padding: 28rpx;
}
.header {
display: flex;
flex-direction: column;
padding: 22rpx 8rpx 30rpx;
}
.title {
font-size: 46rpx;
font-weight: 700;
color: #172033;
}
.subtitle {
margin-top: 10rpx;
font-size: 26rpx;
color: #697386;
}
.card {
margin-bottom: 24rpx;
padding: 28rpx;
border-radius: 22rpx;
background: #ffffff;
box-shadow: 0 8rpx 28rpx rgba(30, 54, 86, 0.07);
}
.tip-card {
background: #eef7ff;
}
.card-title,
.section-title {
display: block;
font-size: 31rpx;
font-weight: 600;
color: #172033;
}
.section-title {
margin-top: 26rpx;
font-size: 28rpx;
}
.tip,
.mode-code,
.path-text,
.timing-text {
display: block;
margin-top: 14rpx;
font-size: 25rpx;
line-height: 1.55;
color: #667085;
}
.picker-value {
margin-top: 20rpx;
padding: 22rpx;
border: 1px solid #d8dee8;
border-radius: 14rpx;
font-size: 30rpx;
color: #1d2939;
}
button {
margin-top: 22rpx;
}
.secondary-button {
color: #2457d6;
background: #edf3ff;
}
.preview-image {
width: 100%;
height: 360rpx;
margin-top: 22rpx;
border-radius: 14rpx;
background: #101828;
}
.result-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.mini-button {
margin: 0;
}
.status {
display: block;
margin-top: 18rpx;
padding: 18rpx;
border-radius: 12rpx;
font-size: 26rpx;
line-height: 1.5;
color: #1d4ed8;
background: #eff6ff;
}
.result-text,
.json-text {
display: block;
margin-top: 14rpx;
padding: 18rpx;
border-radius: 12rpx;
font-size: 25rpx;
line-height: 1.6;
color: #344054;
background: #f8fafc;
word-break: break-all;
white-space: pre-wrap;
}
.json-text {
font-family: monospace;
font-size: 22rpx;
}
.field-row {
display: flex;
padding: 16rpx 0;
border-bottom: 1px solid #edf0f4;
}
.field-name {
width: 220rpx;
font-size: 25rpx;
color: #667085;
}
.field-value {
flex: 1;
font-size: 25rpx;
color: #101828;
word-break: break-all;
}
.bottom-space {
height: 60rpx;
}
</style>
uniappx完整示例
<template>
<scroll-view class="page" scroll-y>
<view class="header">
<text class="title">离线 OCR</text>
<text class="subtitle">yt-offlineocr · uni-app x 调用示例</text>
</view>
<view class="card tip-card">
<text class="card-title">功能说明</text>
<text class="tip">图片和识别结果均在设备本地处理,不依赖网络接口。</text>
<text class="tip">普通文本需要手动确认;身份证、银行卡、车牌等结构化类型匹配成功后自动返回。</text>
</view>
<view class="card">
<text class="card-title">1. 选择识别类型</text>
<picker :range="modeLabels" :value="selectedModeIndex" @change="onModeChange">
<view class="picker-value">{{ selectedModeLabel }} ›</view>
</picker>
<text class="mode-code">识别类型:{{ selectedMode }}</text>
</view>
<view class="card">
<text class="card-title">2. 初始化与图片识别</text>
<text class="tip">初始化可选;提前初始化可减少首次图片识别的等待时间。</text>
<button class="action-button" type="primary" :disabled="busy" @click="onInitialize">提前初始化</button>
<button class="action-button secondary-button" :disabled="busy" @click="onChooseImage">选择相册图片并识别</button>
<image v-if="imagePath.length > 0" class="preview-image" :src="imagePath" mode="aspectFit" />
<text v-if="imagePath.length > 0" class="path-text">图片:{{ imagePath }}</text>
</view>
<view class="card">
<text class="card-title">3. 实时扫描</text>
<text class="tip">调用后会先检查相机权限,授权成功后打开实时扫描页面。</text>
<button class="action-button" type="primary" :disabled="busy" @click="onStartScan">打开实时扫描</button>
</view>
<view class="card result-card">
<view class="result-header">
<text class="card-title">调用结果</text>
<button class="mini-button" size="mini" :disabled="busy" @click="onRelease">释放资源</button>
</view>
<text class="status">{{ statusText }}</text>
<template v-if="lastResult != null">
<text class="section-title">完整文字</text>
<text class="result-text">{{ displayResult.text.length > 0 ? displayResult.text : '未识别到文字' }}</text>
<template v-if="displayResult.fields.length > 0">
<text class="section-title">结构化字段</text>
<view v-for="(field, index) in displayResult.fields" :key="index" class="field-row">
<text class="field-name">{{ field.name }}</text>
<text class="field-value">{{ field.value }}</text>
</view>
</template>
<text class="section-title">耗时</text>
<text class="timing-text">检测 {{ displayResult.timing.detectionMs }}ms;识别
{{ displayResult.timing.recognitionMs }}ms;总计 {{ displayResult.timing.totalMs }}ms</text>
</template>
<text class="section-title">完整回调对象</text>
<text class="json-text" selectable>{{ resultJson }}</text>
</view>
<view class="bottom-space" />
</scroll-view>
</template>
<script setup lang="uts">
import {
initializeOfflineOcr,
recognizeOfflineOcr,
startOfflineOcrScan,
releaseOfflineOcr
} from '@/uni_modules/yt-offlineocr'
import {
OfflineOcrMode,
OfflineOcrResult,
OfflineOcrInitResult,
OfflineOcrReleaseResult,
OfflineOcrFail
} from '@/uni_modules/yt-offlineocr/utssdk/interface.uts'
/** picker 展示项;value 必须与插件公开的 OfflineOcrMode 一致。 */
type ModeOption = {
label : string
value : OfflineOcrMode
}
/** 页面所有可选类型。数组在初始化后不会修改,选择状态单独保存在 selectedModeIndex。 */
const modeOptions : Array<ModeOption> = [
{ label: '普通文本', value: 'TEXT' },
{ label: '身份证正面', value: 'ID_CARD_FRONT' },
{ label: '身份证反面', value: 'ID_CARD_BACK' },
{ label: '车牌', value: 'LICENSE_PLATE' },
{ label: '银行卡', value: 'BANK_CARD' },
{ label: '营业执照', value: 'BUSINESS_LICENSE' },
{ label: '社会保障卡', value: 'SOCIAL_SECURITY_CARD' },
{ label: '发票', value: 'INVOICE' },
{ label: 'VIN 车架号', value: 'VIN' },
{ label: '行驶证', value: 'VEHICLE_LICENSE' },
{ label: '手机号码', value: 'PHONE_NUMBER' },
{ label: '自行车牌照', value: 'BICYCLE_PLATE' }
]
/** 防止初始化、图片识别或实时扫描进行期间重复点击操作按钮。 */
const busy = ref(false)
/** 最近一次通过 uni.chooseImage 选择的本地图片路径。 */
const imagePath = ref('')
/** 最近一次成功结果;null 表示尚未成功识别。 */
const lastResult = ref<OfflineOcrResult | null>(null)
/**
* 模板展示的非空结果占位值。
*
* uvue 模板不会因 v-if 自动把 OfflineOcrResult | null 收窄为非空类型,
* 因此通过 displayResult 统一提供非空对象;实际内容仍只在 lastResult 非空时显示。
*/
const emptyResult : OfflineOcrResult = {
status: '',
mode: 'TEXT',
text: '',
lines: [],
fields: [],
warnings: [],
timing: {
modelLoadMs: 0,
detectionMs: 0,
recognitionMs: 0,
totalMs: 0
},
rawJson: ''
}
/** 将 success/fail 回调对象序列化后展示,便于调试与核对字段。 */
const resultJson = ref('尚未调用')
/** 当前操作的用户可读状态提示。 */
const statusText = ref('请选择识别类型,然后初始化、选择图片或打开实时扫描。')
/** picker 的选中下标。 */
const selectedModeIndex = ref(0)
/** 为模板提供永不为 null 的识别结果,规避 uvue 模板的可空类型限制。 */
const displayResult = computed(() : OfflineOcrResult => {
const result = lastResult.value
return result != null ? result : emptyResult
})
/** picker 的文字数组。UTS 中显式循环创建数组,避免依赖 JS 动态数组推断。 */
const modeLabels = computed(() : Array<string> => {
const labels : Array<string> = []
for (let index = 0; index < modeOptions.length; index++) {
labels.push(modeOptions[index].label)
}
return labels
})
/** 当前选择项。下标始终由 picker 返回值限制在 modeOptions 的有效范围内。 */
const selectedOption = computed(() : ModeOption => {
return modeOptions[selectedModeIndex.value]
})
/** 传给插件的识别类型。 */
const selectedMode = computed(() : OfflineOcrMode => {
return selectedOption.value.value
})
/** 页面展示用的识别类型名称。 */
const selectedModeLabel = computed(() : string => {
return selectedOption.value.label
})
/**
* 将回调数据保存到页面并打印日志。
* JSON.stringify 只使用一个参数,兼容 uni-app x 的 UTS 运行环境。
*/
function showCallback(callbackName : string, value : any) : void {
resultJson.value = JSON.stringify(value)
console.log('[yt-offlineocr] ' + callbackName + ': ' + resultJson.value)
}
/**
* uni-app x Android 的 picker value 运行时为 number,不能强转成 string。
* 直接读取 number 后更新当前类型并清空旧结果。
*/
function onModeChange(event : UniPickerChangeEvent) : void {
const index = event.detail.value as number
if (index < 0 || index >= modeOptions.length) {
return
}
selectedModeIndex.value = index
lastResult.value = null
statusText.value = '已选择:' + selectedModeLabel.value
}
/** 提前初始化识别资源。初始化为异步操作,结束状态由 complete 统一恢复按钮。 */
function onInitialize() : void {
busy.value = true
statusText.value = '正在初始化…'
initializeOfflineOcr({
cpuThreadCount: 4,
detectionThreshold: 0.3,
detectionBoxThreshold: 0.6,
recognitionBatchSize: 1,
maxImageDimension: 2560,
success: (result : OfflineOcrInitResult) => {
statusText.value = '初始化成功,耗时 ' + result.modelLoadMs + 'ms'
showCallback('initialize success', result)
},
fail: (error : OfflineOcrFail) => {
statusText.value = '初始化失败:' + error.errMsg
showCallback('initialize fail', error)
},
complete: () => {
busy.value = false
}
})
}
/**
* 调用插件识别指定图片;结构化类型的字段会在 result.fields 中返回。
*
* 此函数必须声明在 onChooseImage 之前。UTS 不支持此处的函数前置引用,
* 否则在 chooseImage success 回调中无法解析 recognizeImage 名称。
*/
function recognizeImage(path : string) : void {
busy.value = true
lastResult.value = null
statusText.value = '正在识别' + selectedModeLabel.value + '…'
recognizeOfflineOcr({
imagePath: path,
mode: selectedMode.value,
minConfidence: 0.35,
success: (result : OfflineOcrResult) => {
lastResult.value = result
statusText.value = '识别完成,共 ' + result.lines.length + ' 行,' + result.fields.length + ' 个结构化字段。'
showCallback('recognize success', result)
},
fail: (error : OfflineOcrFail) => {
statusText.value = '图片识别失败:' + error.errMsg
showCallback('recognize fail', error)
},
complete: () => {
busy.value = false
}
})
}
/** 从相册选择一张图片,成功后把临时路径直接传给插件。 */
function onChooseImage() : void {
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: (chooseResult) => {
const path = chooseResult.tempFilePaths[0]
imagePath.value = path
recognizeImage(path)
},
fail: (error) => {
statusText.value = '未选择图片:' + error.errMsg
showCallback('choose image fail', error)
}
})
}
/** 打开实时扫描页;插件会按 requestCameraPermission 申请相机运行时权限。 */
function onStartScan() : void {
busy.value = true
lastResult.value = null
statusText.value = '正在打开' + selectedModeLabel.value + '实时扫描…'
startOfflineOcrScan({
mode: selectedMode.value,
minConfidence: 0.35,
hint: '',
stableFrameCount: 2,
requestCameraPermission: true,
success: (result : OfflineOcrResult) => {
lastResult.value = result
statusText.value = '实时扫描成功,返回 ' + result.fields.length + ' 个结构化字段。'
showCallback('scan success', result)
},
fail: (error : OfflineOcrFail) => {
statusText.value = '实时扫描未完成:' + error.errMsg
showCallback('scan fail', error)
},
complete: () => {
busy.value = false
}
})
}
/** 手动释放图片识别资源;释放后仍可再次初始化或继续识别。 */
function onRelease() : void {
releaseOfflineOcr({
success: (result : OfflineOcrReleaseResult) => {
lastResult.value = null
statusText.value = result.message
showCallback('release success', result)
}
})
}
/** 页面销毁时释放资源,避免离开示例页后继续占用内存。 */
onUnload(() => {
releaseOfflineOcr({})
})
</script>
<style>
.page {
flex: 1;
box-sizing: border-box;
padding: 28rpx;
background: #f3f5f8;
}
.header {
display: flex;
flex-direction: column;
padding: 22rpx 8rpx 30rpx;
}
.title {
font-size: 46rpx;
font-weight: 700;
color: #172033;
}
.subtitle {
margin-top: 10rpx;
font-size: 26rpx;
color: #697386;
}
.card {
margin-bottom: 24rpx;
padding: 28rpx;
border-radius: 22rpx;
background: #ffffff;
box-shadow: 0 8rpx 28rpx rgba(30, 54, 86, 0.07);
}
.tip-card {
background: #eef7ff;
}
.card-title,
.section-title {
font-size: 31rpx;
font-weight: 600;
color: #172033;
}
.section-title {
margin-top: 26rpx;
font-size: 28rpx;
}
.tip,
.mode-code,
.path-text,
.timing-text {
margin-top: 14rpx;
font-size: 25rpx;
line-height: 1.55;
color: #667085;
}
.picker-value {
margin-top: 20rpx;
padding: 22rpx;
border: 1px solid #d8dee8;
border-radius: 14rpx;
font-size: 30rpx;
color: #1d2939;
}
.action-button {
margin-top: 22rpx;
}
.secondary-button {
color: #2457d6;
background: #edf3ff;
}
.preview-image {
width: 100%;
height: 360rpx;
margin-top: 22rpx;
border-radius: 14rpx;
background: #101828;
}
.result-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.mini-button {
margin: 0;
}
.status {
margin-top: 18rpx;
padding: 18rpx;
border-radius: 12rpx;
font-size: 26rpx;
line-height: 1.5;
color: #1d4ed8;
background: #eff6ff;
}
.result-text,
.json-text {
margin-top: 14rpx;
padding: 18rpx;
border-radius: 12rpx;
font-size: 25rpx;
line-height: 1.6;
color: #344054;
background: #f8fafc;
white-space: pre-wrap;
}
.json-text {
font-family: monospace;
font-size: 22rpx;
}
.field-row {
display: flex;
padding: 16rpx 0;
border-bottom: 1px solid #edf0f4;
}
.field-name {
width: 220rpx;
font-size: 25rpx;
color: #667085;
}
.field-value {
flex: 1;
font-size: 25rpx;
color: #101828;
}
.bottom-space {
height: 60rpx;
}
</style>

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 617
赞赏 15
下载 12607316
赞赏 1949
赞赏
京公网安备:11010802035340号