Node.js脚本项目合集(三):Node.js+Tesseract.js多线程实现ocr文字识别
前言
上一期Node.js+Tesseract.js单线程实现ocr文字识别通过单线程(即创建一个worker)来借助tesseract.js实现单张图片ocr文字识别。实际的开发中往往会效率低下,识别一张图片都需要20s左右。上一期还有一个问题,每次识别的图片都需要修改代码进行传参,这期为了方便演示,我们将多线程实现ocr文字识别的代码用koa包成http请求,通过传参的方式来进行功能调用。
一、准备工作以及介绍
1、什么是koa
koa是一个Node.js的一个开源web框架,其项目地址koa,官网地址koa官网,这里我们需要koa做一个简单的get请求,方便传参并执行脚本。
2、tesseract / tesseract.js相关
可直接去上一期安装下载,博客链接:Node.js+Tesseract.js单线程实现ocr文字识别
3、创建项目文件
(1)新建lang-data文件夹,将要识别的语言包(.gz后缀名)放到lang-data中(这里我们用4.0_best版本语言包下载4.0_best),运行代码,通过给loadLanguage方法传参(通过语言包code并且用加号“+”拼接的方式引入多国语言)将语言包解析成.traineddata文件。
(2)新建images文件夹,将要识别的图片放到该文件夹下。
(3)新建moreThreads.js文件,用来执行Node.js代码
4、node环境准备
(1)node版本16.0.0以上
(2)需要用到tesseract.js模块、fs模块、moment模块、koa相关模块和path模块
5、postman工具
通过postman工具进行接口测试,并测试实际执行效率情况
二、项目代码
1.VScode中launch.json相关配置
(1) VScode通过launch.json运行:本地使用Visual Studio Code这个工具生成launch.json来进行本地debug,可以直接添加配置。
代码如下(示例):
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "moreThreads",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\moreThreads.js"
}
]
}
2.代码部分
(1)利用koa新增moreThreadsOCR的get请求接口:利用CommonJs方式引用koa和koa-router,新增moreThreadsOCR的get请求接口,并通过3002端口号进行监听,这里我们将workerNum(worker数量)、fileDir(放图片的文件路径)、language(语言类型)通过接口传参的方式接收。
代码如下(示例):
const Koa = require('koa')
const Router = require('koa-router')
const router = new Router()
const app = new Koa()
const path = require('path')
const moment = require('moment')
const { createWorker, createScheduler } = require('tesseract.js');
const fs = require('fs');
router.get('/moreThreadsOCR', async (ctx) => {
// 1、开始获取参数
const { workerNum, fileDir, language } = ctx.query;
// ......业务代码......
// 2、将输出结果返回
ctx.body = res;
})
app.use(router.routes(), router.allowedMethods())
app.listen(3002);
(2)检查图片文件夹的格式: 将传参的文件夹格式中所有的文件遍历一下,并将符合图片后缀的数据添加到filesDirArr数组中
// 2、将图片文件夹中的图片遍历出来
const filesName = fs.readdirSync(fileDir, {withFileTypes: 'png'})
const filesDirArr = filesName.reduce((p, c) =>{
if (new RegExp(/\.(png|jpg|gif)$/, 'g').test(c.name)){
p.push(`${fileDir}\\${c.name}`)
}
return p;
}, []);
(3)通过Tesseract.js异步多线程执行ocr识别: 首先,通过createScheduler方法创建一个调度任务,然后通过传参workerNum数量生成worker数(这里边的worker数就好比工人,生成的越多,后边在执行图片文字识别的线程就越多,效率越快。但是workNum也不是越多越好,因为生成worker的时候需要消耗cpu分配资源,具体数量多少合适后边会给大家详细测试)
// 3、起调度方法,并获得指定数量的worker
const scheduler = createScheduler();
for (let i = 0; i < workerNum; i++) {
const worker = createWorker({
langPath: path.join(__dirname, './lang-data'),
cachePath: path.join(__dirname),
logger: m => console.log(`${moment().format('YYYY-MM-DD HH:mm:ss')}-${JSON.stringify(m)}`)
})
await worker.load()
await worker.loadLanguage(language)
await worker.initialize(language)
scheduler.addWorker(worker)
}
(4)将待识别的图片通过Pormise.all同步输出结果: 异步调用,将这些文件通过Pormise.all同步输出结果,返回OCRres数组,这个数组里每一个text则是最终识别的文本。执行完毕后终止调度任务,防止内存溢出。
// 4、异步调用执行
const OCRres = await Promise.all(filesDirArr.map((fileDirItem) => (
scheduler.addJob('recognize', fileDirItem)
)))
// 5、终止调度任务
await scheduler.terminate();
(5)处理识别结果,返回结果内容: 整理数据结果,返回按照每个文字和文字的坐标的结果输出,其中words中text表示文字、box表示该文字的坐标,confidence表示相似程度的百分比
// 6、处理识别结果,并返回结果内容
const res = OCRres.reduce((p, c) => {
const item = {
text: c.data.text,
words: [],
}
c.data.words.map((m) => {
const wordsItem = {text:'', box:{}, confidence:''};
wordsItem.box = m.bbox;
wordsItem.text = m.text;
wordsItem.confidence = m.confidence;
item.words.push(wordsItem);
})
p.push(item);
return p;
}, []);
(6)综上,完整代码块如下:
const Koa = require('koa')
const Router = require('koa-router')
const router = new Router()
const app = new Koa()
const path = require('path')
const moment = require('moment')
const { createWorker, createScheduler } = require('tesseract.js');
const fs = require('fs');
router.get('/moreThreadsOCR', async (ctx) => {
// 1、开始获取参数
const { workerNum, fileDir, language } = ctx.query;
// 2、将图片文件夹中的图片遍历出来
const filesName = fs.readdirSync(fileDir, {withFileTypes: 'png'})
const filesDirArr = filesName.reduce((p, c) =>{
if (new RegExp(/\.(png|jpg|gif)$/, 'g').test(c.name)){
p.push(`${fileDir}\\${c.name}`)
}
return p;
}, []);
// 3、起调度方法,并获得指定数量的worker
// 备注:这里的workNum不是越多越好,因为启动越多的worker,在createWorker的时候就需要遍历,并挨个创建worker
// 这就好比你创建很多的worker工人,但是图片没有那么多,前期投入很大,但是没有那么多活需要做,所以尽量保证worker数跟cpu数一样,发挥最大作用
const scheduler = createScheduler();
for (let i = 0; i < workerNum; i++) {
const worker = createWorker({
langPath: path.join(__dirname, './lang-data'),
cachePath: path.join(__dirname),
logger: m => console.log(`${moment().format('YYYY-MM-DD HH:mm:ss')}-${JSON.stringify(m)}`)
})
await worker.load()
await worker.loadLanguage(language)
await worker.initialize(language)
scheduler.addWorker(worker)
}
// 4、异步调用,将这些文件通过Pormise.all同步输出结果,返回OCRres数组,这个数组里每一个text则是最终识别的文本
// 这里箭头后面紧跟这一个"()"指的是自调用表达式,包围一些需要通过运算得出结果的代码(其中包围的代码scheduler.addJob('recognize', fileDirItem)会执行一次)。函数表达式可以自调用(即自动运行一次)。如果表达式后面紧跟(),会自动调用。不能自调用声明的函数,通过添加括号,来说明他是一个函数表达式。
const OCRres = await Promise.all(filesDirArr.map((fileDirItem) => (
scheduler.addJob('recognize', fileDirItem)
)))
// 下面是错误写法,
// 原因:箭头函数表达式(ES6)的返回值
// 箭头函数表达式x => x,表示function(x) {return x;}。
// 但如果返回值是object类型,则不能为x => {name:'JT'},,需要改为x => ({name:'JT'})。
// 这里scheduler.addJob是一个函数,其该函数返回的是RecognizeResult为promise对象,所以应该在他的外部再加一个括号可作为匿名函数
// const OCRres = await Promise.all(filesDirArr.map((fileDirItem) =>{
// scheduler.addJob('recognize', fileDirItem)
// }))
console.log(OCRres);
// 5、终止调度任务
await scheduler.terminate();
// 6、处理识别结果,并返回结果内容
const res = OCRres.reduce((p, c) => {
const item = {
text: c.data.text,
words: [],
}
c.data.words.map((m) => {
const wordsItem = {text:'', box:{}, confidence:''};
wordsItem.box = m.bbox;
wordsItem.text = m.text;
wordsItem.confidence = m.confidence;
item.words.push(wordsItem);
})
p.push(item);
return p;
}, []);
ctx.body = res;
})
app.use(router.routes(), router.allowedMethods())
app.listen(3002);
3.完整项目地址
https://github.com/lp970703/node_job/tree/master/tesseractOCR_js
三、思考与分析
1、测试worker数对识别效率的影响
- worker数主要跟cpu核心数有一定关系,通常cpu几核那么worker数就是多少。目前我们测试的这台电脑cpu为16核。
- 下图为我们批量ocr识别10张图片的输出结果时间,单线程用时2分31秒,而设置到8个或者16个worker数时(与CPU核数一致)用时仅为48s左右,而当worker数设置为32个时,所用时间又变大到1分零2秒
2、对返回结果box文字坐标位置的理解
- Tesseract坐标规则:从图片的左上角点为原点,横坐标自左向右x依次变大,纵坐标自上而下y依次变大。x0y0指的是文字中左上角的点,x1y1指的是文字中右下角的点。下图图一为返回的“颜色”这两个字的通过Tesseract返回的坐标位置,图二为用工具实际测出的位置(会出现略微的误差)。
总结
以上就是通过Node.js+tesseract.js多线程实现ocr文字识别,后续还会更新将图片中的文字用方框框出、自己训练ocr语言模型来应对复杂的文字识别等脚本。本人还开放了其他关于nodejs实用脚本,感兴趣可以去本人github地址:node_job中学习。