古籍OCR前端实现:Vue+OpenCV.js浏览器端全流程处理
发布时间:2026/9/16 17:47:35 作者:尧图编辑部 阅读量:1,286

简介本资源是一套基于VueJavaScript实现的古籍文字检测与识别系统完整源码面向计算机类专业本科生、研究生及初学者适用于毕业设计、课程设计、大作业及项目立项演示等实践场景。系统采用vue-cli构建支持本地快速启动与多环境发布涵盖前端检测界面、识别结果展示及基础交互逻辑具备良好的可扩展性与教学适配性。压缩包共106个文件以41个JS逻辑文件、32个Vue组件为核心辅以SVG图标、SCSS样式、YML配置及HTML/JSON等辅助文件结构清晰、模块分工明确总大小仅687KB轻量易部署。目前已有285人学习下载资源经实测可稳定运行附带详细构建说明、开发调试指南与代码规范检查脚本便于理解前后端协作流程、掌握OCR前端集成方法并为二次开发提供扎实基础。1. 古籍 OCR 不是把图片扔进模型就完事Vue 前端要管预处理、结果渲染、交互反馈三件事古籍文字检测与识别系统表面看是“OCR 任务”但实际落地时90% 的失败不是模型不准而是前端没兜住——扫描件倾斜、墨迹洇染、版心歪斜、竖排右起、夹注小字、朱砂批校……这些在印刷体 OCR 里被忽略的细节在古籍场景下全成了拦路虎。本项目用 Vue JavaScript 实现完整前端闭环不调用远程 API所有图像预处理灰度化、二值化、倾斜校正、文字区域检测基于轮廓连通域分析、单字识别轻量 CNN 模型推理全部在浏览器内完成识别结果支持逐字定位高亮、原文与释文双栏对照、点击跳转原图位置、导出带坐标的 JSON 结构化数据。适合古籍数字化团队做本地化快速验证也适合作为高校数字人文课程的可调试教学案例——你不需要部署 Python 环境解压即开拖入一张《永乐大典》残页截图3 秒内看到检测框和识别字。2. 用 Canvas OpenCV.js 在 Vue 中实现古籍图像预处理链古籍图像质量参差极大手机翻拍有阴影、旧扫描件有噪点、胶片扫描带划痕、PDF 截图含压缩伪影。直接送入识别模型会导致漏检率飙升。本项目在 Vue 组件中构建了可配置的前端预处理流水线核心依赖 OpenCV.jsWebAssembly 版避免后端依赖所有操作在用户本地完成。2.1 初始化 OpenCV.js 并加载图像到 CanvasOpenCV.js 需异步加载不能在mounted中直接调用cv.imread。项目采用 Promise 封装初始化逻辑并监听onRuntimeInitialized事件// utils/opencvLoader.js export const loadOpenCV () { return new Promise((resolve) { if (window.cv window.cv.version) { resolve(window.cv); return; } const script document.createElement(script); script.src /js/opencv.js; // 静态资源需提前放入 public/js/ script.onload () { window.cv.onRuntimeInitialized () { resolve(window.cv); }; }; document.head.appendChild(script); }); }; // components/OcrProcessor.vue import { loadOpenCV } from /utils/opencvLoader; export default { data() { return { cv: null, srcMat: null, processedMat: null }; }, async mounted() { this.cv await loadOpenCV(); this.initCanvas(); }, methods: { initCanvas() { const canvas this.$refs.canvas; this.ctx canvas.getContext(2d); this.width canvas.width; this.height canvas.height; }, loadImage(file) { const reader new FileReader(); reader.onload (e) { const img new Image(); img.onload () { // 缩放至最大宽度 1200px 保持宽高比避免大图卡顿 const scale Math.min(1200 / img.width, 1); this.width img.width * scale; this.height img.height * scale; this.$refs.canvas.width this.width; this.$refs.canvas.height this.height; this.ctx.drawImage(img, 0, 0, this.width, this.height); // 转为 OpenCV Mat 格式 this.srcMat this.cv.matFromImageData(this.ctx.getImageData(0, 0, this.width, this.height)); }; img.src e.target.result; }; reader.readAsDataURL(file); } } };提示matFromImageData返回的是 BGR 格式 Mat后续所有cv.cvtColor必须指定cv.COLOR_RGBA2BGR或cv.COLOR_RGB2BGR否则颜色通道错乱导致二值化失效。2.2 构建可调节的古籍专用预处理函数链针对古籍特性预处理链包含四步① 去阴影Top-hat 变换② 自适应二值化cv.adaptiveThreshold③ 倾斜校正霍夫变换检测主直线④ 连通域过滤剔除小于 15×15 像素的噪点。每步参数均暴露为 Vue 响应式变量支持实时调试参数名类型默认值说明shadowKernelSizenumber25Top-hat 形态学核尺寸越大去阴影越强但可能损失细笔画blockSizenumber31自适应阈值块大小必须为奇数古籍常用 21–41Cnumber10自适应阈值常数偏移正值增强对比负值易过曝minLineLengthnumber100霍夫直线检测最小长度低于此值的线不参与角度计算minAreanumber225连通域最小面积px²过滤墨点、虫蛀孔洞processImage() { if (!this.srcMat) return; // 1. 去阴影Top-hat 变换原图 - 开运算 const kernel this.cv.getStructuringElement(this.cv.MORPH_RECT, new this.cv.Size(this.shadowKernelSize, this.shadowKernelSize)); const opened new this.cv.Mat(); this.cv.morphologyEx(this.srcMat, opened, this.cv.MORPH_OPEN, kernel); const tophat new this.cv.Mat(); this.cv.subtract(this.srcMat, opened, tophat); // 2. 转灰度并自适应二值化 const gray new this.cv.Mat(); this.cv.cvtColor(tophat, gray, this.cv.COLOR_BGR2GRAY); const binary new this.cv.Mat(); this.cv.adaptiveThreshold( gray, binary, 255, this.cv.ADAPTIVE_THRESH_GAUSSIAN_C, this.cv.THRESH_BINARY, this.blockSize, this.C ); // 3. 倾斜校正霍夫直线检测主方向 const lines new this.cv.Mat(); this.cv.HoughLinesP( binary, lines, 1, Math.PI / 180, 100, // 阈值 this.minLineLength, 10 ); let angle 0; if (lines.rows 0) { const angles []; for (let i 0; i lines.rows; i) { const line lines.data32S.slice(i * 4, i * 4 4); const dx line[2] - line[0]; const dy line[3] - line[1]; angles.push(Math.atan2(dy, dx) * 180 / Math.PI); } // 取众数角度排除异常值后 angle this.getDominantAngle(angles); } // 4. 旋转校正 连通域过滤 const rotated this.rotateMat(binary, angle); const filtered this.filterSmallComponents(rotated, this.minArea); this.processedMat filtered; this.renderMatToCanvas(filtered); }getDominantAngle函数对角度数组做 10° 区间分桶统计取最高频区间中心值rotateMat使用cv.getRotationMatrix2D生成仿射矩阵filterSmallComponents先cv.findContours再遍历cv.contourArea过滤。这些函数均封装在utils/imageUtils.js中确保逻辑复用。3. 基于轮廓分析的古籍文字区域检测与坐标映射古籍版式复杂有界格、鱼尾、象鼻、天头地脚、行间批注。传统 OCR 的“从左到右逐行切分”完全失效。本项目采用两阶段检测先用连通域Connected Components粗筛候选区域再用轮廓Contour几何特征精筛最终输出符合古籍文字规律的 bounding box 列表。3.1 连通域初筛排除版心外干扰与单字粘连OpenCV 的cv.connectedComponentsWithStats比cv.findContours更适合古籍——它能一次性返回每个连通区域的面积、质心、外接矩形且对微小噪点如纸张纤维天然鲁棒。关键在于设置合理的面积阈值和长宽比约束detectTextRegions() { if (!this.processedMat) return []; // 获取连通域统计信息 const stats new this.cv.Mat(); const centroids new this.cv.Mat(); const nLabels this.cv.connectedComponentsWithStats( this.processedMat, stats, centroids, 8, this.cv.CV_32S ); const regions []; // 跳过背景标签label 0 for (let i 1; i nLabels; i) { const x stats.data32S[i * 5 0]; const y stats.data32S[i * 5 1]; const width stats.data32S[i * 5 2]; const height stats.data32S[i * 5 3]; const area stats.data32S[i * 5 4]; // 古籍单字典型尺寸宽高比 0.6–1.8面积 100–5000 px² const aspectRatio width / height; if ( area 100 area 5000 aspectRatio 0.6 aspectRatio 1.8 width 10 height 10 ) { regions.push({ x, y, width, height, area }); } } return regions; }注意connectedComponentsWithStats返回的坐标是相对于当前 Mat 的而 Canvas 渲染需映射回原始图像尺寸。项目通过scale this.width / this.originalWidth记录缩放比在renderRegionOverlay中将x,y,width,height乘以scale后绘制。3.2 轮廓精筛合并相邻字符与识别竖排结构初筛结果存在两个问题① 行内相邻字未合并如“之乎者也”四字紧贴② 无法区分正文与夹注小字旁批。解决方案是二次聚类对初筛 bbox 按 Y 坐标聚类为“行”再在每行内按 X 坐标排序计算相邻 bbox 的水平间距。若间距 字宽 × 0.7则合并为一个 regionmergeAdjacentRegions(regions) { if (regions.length 0) return []; // 按 y 中心点聚类为行 const rows {}; regions.forEach(r { const centerY r.y r.height / 2; const rowKey Math.round(centerY / 10); // 以 10px 为行距容差 if (!rows[rowKey]) rows[rowKey] []; rows[rowKey].push(r); }); const merged []; Object.values(rows).forEach(row { // 按 x 排序 row.sort((a, b) a.x - b.x); let current { ...row[0] }; for (let i 1; i row.length; i) { const next row[i]; const gap next.x - (current.x current.width); // 若间隙小于当前字宽的 70%则合并 if (gap current.width * 0.7) { current.width next.x next.width - current.x; current.height Math.max(current.height, next.height); } else { merged.push({ ...current }); current { ...next }; } } merged.push({ ...current }); }); return merged; }对于竖排文本如《四库全书》项目额外提供“列模式”开关此时按 X 坐标聚类为列再在列内按 Y 排序合并垂直相邻区域。该模式通过this.layoutMode vertical控制UI 上提供单选按钮切换。4. 在浏览器中运行轻量 CNN 模型完成单字识别识别模块不依赖 TensorFlow.js 完整框架体积过大而是使用 ONNX Runtime Web 加载训练好的轻量 CNN 模型ancient-char-recognizer.onnx输入为 48×48 灰度图输出 3000 类古籍常用字含异体字、避讳字的概率分布。4.1 模型加载与输入预处理ONNX Runtime Web 支持 WebAssembly 后端启动快、内存占用低。关键步骤① 预加载模型② 将 Canvas 区域裁剪、缩放、归一化为模型输入 tensor// utils/onnxLoader.js import { InferenceSession } from onnxruntime-web; export class CharRecognizer { constructor(modelPath) { this.session null; this.modelPath modelPath; } async init() { this.session await InferenceSession.create(this.modelPath, { executionProviders: [wasm], graphOptimizationLevel: all }); } async recognizeChar(canvas, x, y, width, height) { // 1. 从 canvas 裁剪区域 const imageData canvas.getContext(2d).getImageData(x, y, width, height); // 2. 转为灰度并缩放到 48x48 const resized this.resizeGrayscale(imageData, 48, 48); // 3. 归一化(pixel - 128) / 128 → [-1, 1] const inputArray new Float32Array(resized.length); for (let i 0; i resized.length; i) { inputArray[i] (resized[i] - 128) / 128; } // 4. 构造 ONNX 输入 tensor[1, 1, 48, 48] const inputTensor new ort.Tensor(float32, inputArray, [1, 1, 48, 48]); const feeds { input: inputTensor }; const output await this.session.run(feeds); const scores output.output.data; // 5. 取 top-3 索引及概率 return this.getTopK(scores, 3); } }resizeGrayscale函数使用双线性插值比 CanvasdrawImage缩放更保真getTopK对scores数组排序并映射回字符集charSet.json文件定义 Unicode 码位与字形对应关系。4.2 识别结果与 UI 的双向绑定策略识别结果需实时反馈给用户但全量识别耗时长单字约 80ms。项目采用“懒识别”策略仅当用户点击某个检测框时才触发该区域识别识别中显示...占位符成功后更新region.char和region.confidence并自动聚焦下一个未识别区域。Vue 模板中div v-for(region, idx) in regions :keyidx classregion-box :styleregionStyle(region) clicktriggerRecognition(idx) span v-ifregion.char{{ region.char }}/span span v-else-ifregion.recognizing.../span span v-else?/span div classconfidence-bar :style{ width: ${region.confidence * 100}% }/div /divregionStyle计算绝对定位left: ${region.x}px; top: ${region.y}px; width: ${region.width}px; height: ${region.height}px确保与 Canvas 上的检测框像素级重合。5. 导出结构化结果与古籍专用校对工作流识别完成只是开始古籍整理的核心是“可校对”。本系统导出的不是纯文本而是带空间坐标的 JSON支持导入专业古籍校勘平台如“籍合网”兼容格式并内置简易校对界面。5.1 导出 JSON 结构保留版式语义与置信度导出数据严格遵循古籍整理规范字段含义明确{ sourceImage: yongledadian_001.jpg, width: 1200, height: 1800, scale: 0.85, regions: [ { x: 234, y: 156, width: 42, height: 48, char: 永, confidence: 0.923, lineIndex: 0, charIndex: 0, isAnnotation: false }, { x: 288, y: 156, width: 40, height: 46, char: 樂, confidence: 0.871, lineIndex: 0, charIndex: 1, isAnnotation: false } ] }lineIndex和charIndex由前端按检测顺序自动生成isAnnotation字段通过右键菜单手动标记小字批注、朱砂圈点等导出时保留。点击“导出 JSON”按钮触发exportResults() { const dataStr JSON.stringify({ sourceImage: this.fileName, width: this.width, height: this.height, scale: this.scale, regions: this.regions.map((r, idx) ({ x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height), char: r.char || , confidence: r.confidence || 0, lineIndex: Math.floor(idx / 20), // 每行约 20 字估算 charIndex: idx % 20, isAnnotation: r.isAnnotation || false })) }, null, 2); const blob new Blob([dataStr], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download ocr-result-${Date.now()}.json; a.click(); URL.revokeObjectURL(url); }5.2 内置校对模式双屏对照与快捷键修正校对界面左侧显示原图Canvas与检测框右侧显示结构化文本支持键盘快捷键高效修正快捷键功能说明Tab跳转下一个未识别区域自动聚焦并触发识别CtrlZ撤销上一次修改仅对char和isAnnotation生效Delete清空当前字置为空字符串保留坐标F2手动编辑当前字弹出输入框支持输入 Unicode 字符或粘贴Alt↑/↓微调 Y 坐标 ±1px修正检测框偏移CtrlEnter提交当前行校对触发saveLine事件供外部系统监听校对状态实时保存在localStorage关闭页面不丢失。当用户修改region.char时confidence自动置为null导出 JSON 中该字段为0明确标识人工干预项。这一设计让机器识别与人工校对形成闭环而非替代关系——这正是古籍数字化项目可持续推进的关键。本文还有配套的精品资源点击获取