ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python+OCR实现乐高批量入库:本地库存盘点全流程

用Python+OCR实现乐高批量入库:本地库存盘点全流程 跑了一趟石家庄带回来70余套乐高。后备箱塞满后真正费时间的不是搬运而是盘点这一堆盒子里都有什么编号、哪些缺件、哪些重复、放在哪个箱子、之后补件或出掉怎么查。逐个手工登记别说时间光是把编号从盒子角落抄下来就够烦。我干脆把“日常vlog拍回来的乐高批量入库”这件事做成了一个本地数据处理流程用手机统一拍照再用 Python OCR 批量识别包装盒上的套装编号最后落到 SQLite 里做库存管理顺带开一个本地网页随时查。这篇不是乐高开箱评测而是完整记录这套“乐高库存盘点方案”从拍照规范、OCR识别、数据入库到本地查询的实现思路。如果你手头也有大量实体收藏需要数字化管理比如积木、模型、书籍、二手商品这套流程可以直接改路径复用。里面涉及的代码我都按照通用版本写了示例具体接口版本不同时按本机的依赖文档调整即可。1. 这套“乐高库存盘点方案”到底解决什么问题先说结论这个方案不是某个大厂开源项目而是把“批量实物入库”最常见的几个环节组合起来能力项说明方案类型本地批量盘点工具Python OCR SQLite Flask核心功能批量识别包装盒套装编号、入库、去重、检索、导出 CSV硬件要求CPU 可运行 OCRGPU 可选需要一部可拍照的手机或数码相机启动方式命令行脚本批量处理 Flask 本地网页查询接口能力可提供本地 API默认建议只绑定 127.0.0.1批量任务支持整个目录批量扫描识别数据存储SQLite 单文件免部署数据库服务支持平台Windows / Linux / macOS只要有 Python 环境适合场景几十上百件实物收藏入库、二手商品盘点、线下收货校验70 余套乐高看似数目不大但手工录入的场景里有几个问题特别明显。第一盒子上的套装编号位置不统一。有的在正面左下角有的在侧面有的是白底字有的是反色字靠眼睛看很容易漏。第二同一套乐高可能会重复收只靠记忆判断“这套是不是已经有了”完全不靠谱。第三库存数据如果只存在 Excel后续想按存放位置、来源、状态筛选很麻烦照片和套装信息也容易脱节。用目录扫描加 OCR 的方式一次拍照后续的编号提取、入库、查重和导出都可以在脚本里完成。所以这篇文章解决的主要问题就是从“一堆实物盒子”变成“一份可查询、可筛选、可导出的数字清单”。中间只需要一套稳定的文件命名规范再加一个能批量运行的 OCR 脚本。2. 适用场景与使用边界这套方案最合适的用户是这几类线下批量收货的收藏爱好者需要对大量实体套装做首次入库。二手商品整理者需要快速给物品拍照登记。家庭或小型工作室的库存管理员不想折腾重量级进销存系统只想在本地完成简单记录。研究 OCR 批量识别的开发者需要一套“真实非标文本”的练习数据。不适合的情况也明确一下。如果数量只有一两套手工写表格反而比搭环境快。如果需要一个多人在线共享、带审批流和财务核算的系统SQLite 加 Flask 也撑不住直接换现成的进销存平台更稳妥。如果拍摄环境光线极差、盒子重度磨损OCR 会频繁识别失败错误率会抵消自动化效率。合规方面要特别注意对乐高包装盒拍照通常是为了个人收藏整理或二手转售时的实物记录这是正常使用场景。但不要未经授权批量搬运官方图片做商用素材也不要在商品介绍里未经许可使用他人的照片。如果后续要把这套流程扩展到人脸照片、证件、隐私文件识别务必换成隐私保护方案并在本机完成处理不要上传到不可控的第三方接口。3. 环境准备与前置条件3.1 硬件与拍照设备正式跑脚本之前先把拍摄端准备好。手机主摄一般就够用条件允许时用微距或补光灯核心要求是盒面信息清晰、不反光。我的建议是把每套乐高单独拍一张正面照如果盒子侧面有完整编号和名称再补一张侧面照。一套产品一个文件夹或者连续编号尽量避免一套盒子十几张图混在一起、OCR 之后还不知道是哪张对应哪套。拍完的照片统一放到一个photos/目录下即可。3.2 Python 环境推荐使用 Python 3.9 到 3.11新版本需要先确认依赖包是否完全兼容。不建议直接装在系统 Python 里创建一个虚拟环境会省掉后面很多麻烦。python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate3.3 安装依赖整个流程依赖三个主要部分OCR 引擎、图片处理库、Web 服务库。PaddleOCR 对 CPU 和 GPU 都可以支持CPU 模式下的识别速度完全够用毕竟这里不是做实时视频流识别。pip install --upgrade pip pip install paddlepaddle paddleocr pip install opencv-python pip install flask如果你本机已有 CUDA 环境可以安装 GPU 版 PaddlePaddle。没有的话不用强求CPU 跑批量图片的体验不会差太多。这里不写死版本号因为 PaddleOCR 迭代较快具体安装命令以项目 README 为准。安装完成后可以先跑一个极简测试确认 OCR 引擎能正常加载。部分 PaddleOCR 版本首次运行会自动下载检测和识别模型如果没有外网访问条件需要在离线机器上提前准备好模型文件并放到指定目录。4. 拍照与素材目录规范4.1 文件命名与目录设计目录规范这件事很土但直接影响后面全自动处理的成功率。我第一次大批量测试时没有固定命名方案结果 OCR 出来的编号和照片对应关系要靠时间戳反推非常折腾。后来改成下面的结构photos/ ├── 20250701_shijiazhuang/ │ ├── 01_42110.jpg │ ├── 01_42110_side.jpg │ ├── 02_76989.jpg │ └── 03_UNKNOWN.jpg ├── 20250703_online/ │ └── 01_10305.jpg日期加地点作为批次目录文件名的第一部分是序号第二部分是人工初判套装编号。编号不确定的可以先写 UNKNOWNOCR 之后会尝试重新识别等到人工复核时再补。这套结构的好处在于批次信息变成了目录名机器可以自动读取图片顺序变成了文件名序号后期如果 OCR 结果要回填照片能直接从文件名拿到关联关系。4.2 拍摄注意事项拍照的时候有三点直接影响识别率。第一光线均匀不要有强反光。塑封膜和纸盒都会反光一旦反光区域压在编号上文字就断。第二编号区域尽量占据画面的主要位置不要用广角把整面墙都拍进去再靠后期裁剪。第三同一批次拍摄不要频繁旋转手机很多老版本图像处理库对 EXIF 方向信息处理不够好你拍的时候看着是正的代码读出来可能是旋转 90 度。如果盒子编号非常小旁边放一张带序号的便利贴一起拍进去后面人工复核时能快速定位。5. 批量 OCR 识别套装编号5.1 OCR 方案选择可选方案大致有三类PaddleOCR、Tesseract、云厂商 OCR 接口。PaddleOCR 的优势是中文和英文都能处理支持方向分类CPU 推理也算快且开源免费。Tesseract 配置简单但对包装盒上带艺术字、斜向排版的文字识别率偏低。云厂商 OCR 接口识别率通常更高但这套场景是几十上百张私人物品照片为了隐私和成本优先走本地 OCR 更合适。下面代码以 PaddleOCR 2.x 常见接口为例。如果你的版本变化导致接口不同参考官方 README 改成对应的调用方式逻辑不用变。5.2 批量识别脚本# batch_ocr.py from pathlib import Path import json import re try: from paddleocr import PaddleOCR except ImportError as exc: raise SystemExit(请先安装 paddleocr 依赖) from exc PHOTO_DIR Path(./photos) OUTPUT_FILE Path(./ocr_results.json) # 加载 OCR 模型 # 参数 use_angle_cls 用于文字方向分类lang 按实际识别语言调整 ocr PaddleOCR(use_angle_clsTrue, langen, show_logFalse) def extract_set_no(texts): 从一行 OCR 文本中提取乐高套装编号。 乐高套装编号一般是连续 4~6 位数字例如 42110、10297。 for text in texts: clean re.sub(r\s, , text) match re.search(r(?!\d)(\d{4,6})(?!\d), clean) if match: return match.group(1) return None def main(): results [] # 遍历所有 jpg/jpeg/png 图片 for image_path in sorted(PHOTO_DIR.rglob(*.jpg)): item { image: str(image_path), set_no: None, raw_texts: [], ok: False, } try: result ocr.ocr(str(image_path), clsTrue) # 有些版本返回多层嵌套这里做一层兼容 if result: lines [] for line_group in result: if line_group is None: continue for line in line_group: lines.append(line[1][0]) item[raw_texts] lines item[set_no] extract_set_no(lines) item[ok] item[set_no] is not None except Exception as exc: # noqa: BLE001 item[error] str(exc) results.append(item) print(f{image_path}: {item[set_no] or NO_SET_NO}) OUTPUT_FILE.write_text( json.dumps(results, ensure_asciiFalse, indent2), encodingutf-8, ) print(f\n完成结果已写入 {OUTPUT_FILE}) if __name__ __main__: main()脚本遍历photos/下所有 JPG调用 OCR 识别图片文本再通过正则提取疑似套装编号。拿 70 套乐高来说每套两张图大约 140 张跑完可能只需要一个午休时间。实际时间取决于图片分辨率和 CPU 性能不用在这步纠结绝对速度。如果你用的 PaddleOCR 是比较新的版本ocr.ocr(img, clsTrue)可能被标记为废弃新的接口通常叫ocr.predict(img)。建议先用一张图测试当前版本的调用方式确认拿到的是包含文本坐标、文本内容和置信度的列表再放量跑批量。5.3 人工复核兜底OCR 准确率再高也不可能是 100%尤其面对包装盒反光、斜体字、旧款设计字体时识别结果会出现偏差。我处理这批乐高时的策略是先全量跑脚本把识别成功的数量确认下来再用下面的流程筛出需要人工看的图。规则很简单能提取到合法编号的图片标记为ok: true识别不到编号或编号明显的再打开图片人工补录。不要指望一次 OCR 做到零人工。事实上OCR 真正帮你节省的不是全部人工而是把“每张图都要看一遍”降为“只看异常图”。这个效率提升已经很可观。6. 数据入库和去重统计OCR 结果只是中间产物。要让 70 余套乐高变成随时可查的库存下一步是建 SQLite 库。6.1 SQLite 建表-- schema.sql CREATE TABLE IF NOT EXISTS sets ( id INTEGER PRIMARY KEY AUTOINCREMENT, set_no TEXT NOT NULL, series TEXT, name TEXT, pieces INTEGER, condition TEXT DEFAULT unknown, status TEXT DEFAULT in_stock, source TEXT, purchase_date TEXT, box_location TEXT, photo_path TEXT, ocr_raw TEXT, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE INDEX IF NOT EXISTS idx_sets_set_no ON sets(set_no); CREATE INDEX IF NOT EXISTS idx_sets_status ON sets(status); CREATE UNIQUE INDEX IF NOT EXISTS idx_sets_photo ON sets(photo_path);set_no存套装编号series存系列名称condition存状态比如全新、已拆、缺件、已绝版后续统计时很有用。photo_path存本地图片路径这样照片和库存记录能一一对应。对 photo_path 建唯一索引防止同一张照片反复导入。6.2 导入识别结果# import_ocr.py import json import sqlite3 from pathlib import Path JSON_FILE Path(./ocr_results.json) DB_FILE Path(./lego_inventory.db) def main(): data json.loads(JSON_FILE.read_text(encodingutf-8)) conn sqlite3.connect(DB_FILE) conn.execute(PRAGMA journal_modeWAL;) conn.execute(PRAGMA foreign_keysON;) inserted 0 skipped 0 for item in data: if not item.get(ok): skipped 1 continue image_path item[image] set_no item[set_no] source_dir str(Path(image_path).parent) try: conn.execute( INSERT INTO sets (set_no, source, photo_path, ocr_raw) VALUES (?, ?, ?, ?) , (set_no, source_dir, image_path, json.dumps(item[raw_texts], ensure_asciiFalse)), ) inserted 1 except sqlite3.IntegrityError: skipped 1 conn.commit() conn.close() print(f新增 {inserted} 条跳过 {skipped} 条空结果或重复图片) if __name__ __main__: main()导入脚本里唯一索引可以兜底确保同一张照片不会被重复写入。如果整个批次目录需要重新识别直接删掉旧的 JSON 再跑一次就行。6.3 重复照片与重复套装排查入库后可以接着跑一条 SQL看看 70 余套里有多少编号出现了多次。SELECT set_no, COUNT(*) AS cnt FROM sets GROUP BY set_no HAVING cnt 1 ORDER BY cnt DESC;如果set_no重复有两种原因同一个套装拍了正反面多张或者确实收了两套相同编号。区分方式也很简单看photo_path文件名文件名不同批次目录基本就是多套反之一套多图可以删除冗余记录。6.4 导出 CSV数据入库之后CSV 导出要保留最常用的字段。后续想导入 Excel、共享给朋友或做二次处理都方便。# export_csv.py import csv import sqlite3 from pathlib import Path DB_FILE Path(./lego_inventory.db) CSV_FILE Path(./lego_inventory.csv) def main(): conn sqlite3.connect(DB_FILE) cursor conn.execute( SELECT set_no, series, name, condition, status, source, purchase_date, box_location, photo_path FROM sets ORDER BY set_no ) columns [desc[0] for desc in cursor.description] rows cursor.fetchall() with CSV_FILE.open(w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow(columns) writer.writerows(rows) conn.close() print(f已导出 {len(rows)} 条到 {CSV_FILE}) if __name__ __main__: main()utf-8-sig编码是为了让 Excel 打开 CSV 时中文不乱码。如果你只做本机工具这个小细节非常值得记住。7. 本地可视化盘点服务7.1 Flask 启动一个查询页面数据库管理脚本够用了但每次查询都打开命令行不够直观。所以再叠加一个轻量 Flask 服务把库存变成网页表格支持按编号、系列、状态过滤。这里直接返回 JSON API方便你自己接前端或后续扩展成小程序。# app.py import sqlite3 from pathlib import Path from flask import Flask, jsonify, request DB_FILE Path(./lego_inventory.db) app Flask(__name__) def query_db(sql: str, args()): conn sqlite3.connect(DB_FILE) conn.row_factory sqlite3.Row rows conn.execute(sql, args).fetchall() conn.close() return [dict(row) for row in rows] app.get(/api/health) def health(): return {status: ok} app.get(/api/sets) def list_sets(): set_no request.args.get(set_no, ).strip() series request.args.get(series, ).strip() status request.args.get(status, ).strip() sql SELECT * FROM sets WHERE 11 args [] if set_no: sql AND set_no LIKE ? args.append(f%{set_no}%) if series: sql AND series LIKE ? args.append(f%{series}%) if status: sql AND status ? args.append(status) sql ORDER BY id DESC LIMIT 500 return jsonify(query_db(sql, args)) app.get(/api/stats) def stats(): rows query_db( SELECT status, COUNT(*) AS cnt FROM sets GROUP BY status ) total query_db(SELECT COUNT(*) AS cnt FROM sets)[0][cnt] return jsonify({total: total, items: rows}) if __name__ __main__: # 只绑定本机避免暴露到局域网的其它机器 app.run(host127.0.0.1, port5000, debugFalse)启动命令python app.py浏览器打开http://127.0.0.1:5000/api/health能看到正常返回说明服务已经起来了。之后访问/api/sets就能看到完整库存。7.2 接口能力验证前端页面这一步不写太复杂的代码先验证接口能跑通即可。curl http://127.0.0.1:5000/api/health curl http://127.0.0.1:5000/api/stats curl http://127.0.0.1:5000/api/sets?seriestechnic如果你不希望自己写前端页面直接用接口把数据接到 Excel、DataEase、明道云这类工具里都可以。数据写到 SQLite 之后入口就变得开放了。7.3 访问安全性Flask 开发服务器默认不适合直接暴露到公网所以我代码里强制绑定127.0.0.1。如果确实想让手机在同一局域网访问指定host0.0.0.0也能做到但要明白这样的代价局域网内任何设备都能访问到库存接口甚至可能被扫描工具探测。建议不要随意改除非你清楚风险并已经加了访问控制。8. 资源占用与性能观察8.1 单张耗时统计资源占用是这类本地脚本是否值得长期用的关键。在跑完整批照片前先用一小批图片测一下单张处理耗时比较稳妥。Linux 和 macOS 下可以直接用time命令看总耗时Windows PowerShell 里用Measure-Command也行。time python batch_ocr.py更精细的做法是在脚本里记录每张图的耗时输出到 CSV。因为图片分辨率、电脑 CPU 性能和 OCR 版本不同单张耗时会差很多这里不建议按别人的经验猜数字用自己的环境跑一遍最有参考价值。8.2 CPU 与显卡需求OCR 检测和识别模型都不是超大体积CPU 推理完全可行。如果机器有 NVIDIA 显卡且装好 GPU 版 PaddlePaddle速度会更快但如果没有显卡环境也不必因此放弃这套方案。在一大堆照片里单张图慢个一两秒完全可接受。观察 GPU 占用时可以用 NVIDIA 官方工具注意只要 OCR 程序在运行就能看到相关进程。进程结束后显存会自动释放不存在后台常驻模型的问题因为脚本是一次性命令行任务。8.3 大批量场景的降载策略如果后续要扩展到上千张图片建议在脚本里加一个批量切割逻辑每次只处理前 100 张输出一个 JSON避免单次任务内存持续上涨。还可以把高分辨率原图按比例缩小到 1600 像素宽再做 OCR显著降低耗时和显存占用。照片质量足够清晰时尺寸缩小不会明显影响编号识别准确率。更合理的做法是把单张图片处理拆成独立任务空闲 CPU 核足够多的情况下可以用multiprocessing.Pool并行处理。但如果你的 CPU 本来就不强并行反而会因为 IO 和内存抢占降低整体效率。先单线程跑一批测试数据再决定是否并行。9. 常见问题与排查方法问题现象可能原因排查方式解决方案安装 paddleocr 后 import 失败Python 版本过高或依赖冲突检查报错堆栈确认当前虚拟环境使用 Python 3.9~3.11 重新建环境OCR 识别不出编号图片反光 / 文字太小 / 方向不对打开原图检查编号区域清晰度重新补拍调整光线增加补光灯识别出的编号乱码把数字识成了字母查看 OCR 原始文本增加人工复核规则识别后过滤非数字字符脚本报错模板文件不存在PaddleOCR 模型未下载或未指定路径查看报错中的模型路径外网受限时手动下载模型放入指定目录导入数据库时提示重复同一张照片被多次扫描查看 photo_path 文件名和批次目录保留一条记录其余通过唯一索引跳过Flask 页面无法打开服务没启动或端口被占用检查终端日志再试其它端口port5001启动注意防火墙规则OCR 处理大量图片越来越慢图片太大内存占用持续上涨看系统资源占用批量分片处理图片等比缩小CSV 用 Excel 打开中文乱码编码写成了 utf-8查看编辑器编码改成 utf-8-sig 后重新导出需要局域网访问但一直连不上Flask 绑定了 127.0.0.1查看启动代码明确风险后绑定 0.0.0.0并加访问控制一套套装多张图重复统计正反面图片都入库查重复编号列表合并照片到一条记录冗余记录删除出现问题时先看报错信息关键词再缩小范围到几十张图的小批次测试。平时用批量脚本最怕的就是直接拿全部 70 余套照片跑环境万一中间报错排错成本会高很多。10. 避坑建议与最佳实践整套流程跑完后有几个值得沉淀的工程习惯。第一第一次运行先跑小批量。不要一上来就把所有目录的照片都丢给 OCR先准备 3 到 5 张不同类型的照片确认能识别、能入库再放量。第二维护一套最小可运行配置。把依赖安装命令、目录结构、建表 SQL、导入脚本放到同一个项目目录里减少换机器时的重复踩坑。第三照片和数据库分开备份。照片一般体积较大适合按批次打包存移动硬盘或 NAS数据库文件小可以放到云盘备份或者 Git 私有仓库。这样即使一台电脑坏了照片还在数据库也能恢复。第四对condition、status这类字段做下拉约束或枚举说明避免同一个意思分别写成“拆封”“已拆”“开盒”导致统计混乱。SQLite 没有强制枚举检查也可以由应用层控制或者用CHECK约束兜底。第五涉及可能的转售场景发布商品图时应使用本人拍摄的照片避免未经授权使用官方版权图。本文档流程基于个人本地仓库管理思路不涉及绕过任何平台限制的技术手段。11. 后续可扩展方向目前这套流程至少可以朝三个方向继续扩展。方向一是给每套实物生成二维码。把 SQLite 里的 id 和 set_no 做成二维码贴到收纳盒上后续用手机扫码就能跳转本地服务查看详细信息适合库房分区管理。方向二是把照片中的小零件数量识别做起来。乐高缺件补件是高频需求如果可以按零件形态做目标检测顺便统计每个零件盒里有什么零件会比单纯登记套装编号更进一步。这个方向从技术上看就需要更重的目标检测模型和数据标注了。方向三是把识别脚本做成一个常驻 API 服务。这样手机上的小程序或 Web 应用可以直接提交新到货图片后台自动识别编号并写入数据库减少手动跑命令行的频率。接口设计上仍建议保持本机优先不要一上来就把管理服务暴露出网。70 余套乐高能不能在半天内变成一份整齐的数据库取决于拍照是否规范、图片目录是否清晰、识别结果是否有兜底。先把一个最小闭环跑通拍照、OCR、SQLite、Flask 查询。这四步打通后后面加去重、二维码或移动端入口都只是扩展问题。
返回列表