简介:一份基于Python和计算机视觉的仓库货位识别系统实战项目资料,面向具备Python编程基础、熟悉OpenCV及深度学习框架的开发者,解决货架区域定位、货位编号OCR识别和坐标到业务编码映射等典型问题。文档按项目背景、模型架构、代码实现、服务部署顺序展开,覆盖摄像头采集、透视变换校正、YOLO目标检测、PaddleOCR文字识别、多帧确认与置信度融合,并配套FastAPI接口、MySQL/SQLite存储及Streamlit可视化界面。资源仅含1个docx文档,压缩包约101KB,内含数据库建表语句、API规范、核心代码示例与部署方案,结构清晰,便于对照目录逐步实践。目前已有74人在线学习,适合高校学生、初中级研发工程师用于智能制造、电商物流和自动化立体库场景的仓储视觉平台搭建、学习与二次开发。
1. 仓库货位识别系统:从摄像头图像到货位编码的完整落地
做仓储视觉项目最容易被低估的不是模型精度,而是“把像素坐标翻译成业务货位编码”这一整条链路。一个基于Python的仓库货位识别系统,表面上是目标检测加OCR,真正跑起来才发现:光照一变、标签一歪、货箱一挡,识别结果就敢给你编一个不存在的货位号。本文拆的这份项目实例,正好把这条链路完整走了一遍——OpenCV做图像预处理与透视校正,YOLO定位货架、货箱和标签,PaddleOCR读编号,再通过几何映射把检测框坐标换算成“A区R03货架第2层第5列”这种业务编码,最后用FastAPI把结果写进MySQL,用Streamlit做可视化复核。这套设计解决的是仓库盘点、上架校验和异常复核的真实痛点,适合正在做计算机视觉课设、仓储自动化项目或者想入门工业视觉落地的Python开发者。
2. 图像预处理与透视校正:识别精度的第一道关口
2.1 仓库光照问题的常见表现与增强策略
仓库场景的光照问题远比你想象中复杂。顶部日光灯、侧窗自然光、叉车灯、金属货架反光,会在同一条通道里同时出现。摄像头架在通道顶部俯拍时,货架上层光线充足,下层可能被货箱遮挡形成阴影;逆光环境下白色标签和银灰色货架背景几乎融为一体。这些问题如果不处理,YOLO检测和OCR识别都无从谈起。
项目里采用的方案是分阶段增强。推理阶段先用灰度化降低颜色干扰,再用CLAHE做局部对比度增强。CLAHE和普通直方图均衡化的区别在于:普通均衡化是对整张图做全局映射,暗区和亮区互相牵扯;CLAHE把图像分成小块分别做均衡化,再用双线性插值消除块间边界,对局部过暗区域更友好。参数上,clipLimit控制在2.0到3.0之间,tileGridSize用8×8,这两个值在不同光照条件下需要微调。
import cv2 def enhance_image(img, clip_limit=2.5, tile_size=8): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(tile_size, tile_size)) enhanced = clahe.apply(gray) return cv2.cvtColor(enhanced, cv2.COLOR_GRAY2BGR)这段代码的思路是先把BGR图转成灰度,CLAHE只对单通道做处理,之后再转回BGR保持后续流程的接口统一。clipLimit是对比度限制阈值,值越大增强越明显,但过大容易放大噪声;tileSize决定局部区域大小,8×8对1080p图像效果适中,720p可以适当减小。需要注意:对原本光照正常的图片做CLAHE反而会让画面显得发灰,所以项目里会先算平均亮度,低于阈值才执行增强,这个判断逻辑不能省略。
2.2 透视变换:把倾斜标签校正成矩形
摄像头和货架之间几乎不可能完全垂直。顶部安装的摄像头存在俯仰角,叉车上的摄像头随车体晃动,端部安装的摄像头则有侧向夹角。标签在这样的视角下会呈现梯形或任意四边形变形,直接送进OCR会导致字符压扁或拉伸。
透视变换的核心是建立原始四边形四个角点和目标矩形四个角点之间的映射矩阵。OpenCV里的getPerspectiveTransform接收两组点集,计算3×3变换矩阵,再由warpPerspective执行重映射。项目在仓库初始化阶段会让管理员在画面里点选货架四个角点,这些点保存成JSON配置,识别时直接从配置文件读取。
import cv2 import numpy as np def four_point_transform(image, pts): rect = np.array(pts, dtype=np.float32) (tl, tr, br, bl) = rect width_top = np.linalg.norm(tr - tl) width_bottom = np.linalg.norm(br - bl) max_width = max(int(width_top), int(width_bottom)) height_left = np.linalg.norm(bl - tl) height_right = np.linalg.norm(br - tr) max_height = max(int(height_left), int(height_right)) dst = np.array([ [0, 0], [max_width - 1, 0], [max_width - 1, max_height - 1], [0, max_height - 1]], dtype=np.float32) M = cv2.getPerspectiveTransform(rect, dst) warped = cv2.warpPerspective(image, M, (max_width, max_height)) return warped这里计算目标尺寸时分别取了上下边宽度和左右边高度的较大值,是为了防止梯形变形导致校正后图像被截断。实际项目中角点坐标是浮点数,透视变换后浮点坐标需要映射到整数像素位置,所以目标尺寸计算减1避免数组越界。还有个容易忽略的细节:角点顺序必须一致,通常是左上、右上、右下、左下,如果标注时点序混乱,变换结果会翻转或者叠加。
3. YOLO目标检测与货位映射:从像素坐标到业务编码
3.1 数据集标注格式与类别的选择
项目里目标检测的类别设计为rack、bin、box、pallet、label五类。rack是货架整体,bin是货位格,box是货箱,pallet是托盘,label是货位标签。标注格式采用YOLO格式,每张图对应一个同名txt文件,每行是“类别编号 中心点x 中心点y 宽度 高度”,所有坐标归一化到0-1之间。
这里有一个重要的实践经验:label类别是单独标注的,不是只标货箱。因为货位编号通常印在货架横梁的标签上,而不是贴在货箱上。如果只检测货箱再去找编号,俯拍视角下货箱顶部往往没有标签信息。项目里设置这个类别的目的是让OCR模块有明确的输入区域——检测到label框后裁剪出图像区域,再做透视校正和文字识别。
数据采集建议覆盖不同时段、不同角度和不同遮挡程度。训练集、验证集、测试集的划分要特别注意:连续视频帧非常相似,如果随机划分,相邻帧会同时出现在训练集和测试集里,导致验证分数虚高,这叫数据泄漏。项目里按时间片段划分,前80%的帧做训练,后20%做测试,而不是随机打乱。
3.2 YOLO推理与置信度处理
项目使用YOLO系列模型做推理,推理结果包含检测框坐标、类别和置信度。实际部署中模型权重放在models目录下,推理时加载一次权重常驻内存,避免每次请求都重新加载。输入图像需要resize到模型要求的尺寸,640×640是速度和精度的平衡点。
from ultralytics import YOLO model = YOLO('models/yolov8s_rack.pt') def detect_objects(image, conf_threshold=0.45): results = model(image, conf=conf_threshold, verbose=False)[0] detections = [] for box in results.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf[0]) cls = int(box.cls[0]) detections.append({ 'class': model.names[cls], 'bbox': [int(x1), int(y1), int(x2), int(y2)], 'confidence': conf }) return detections置信度阈值0.45是一个经验值。阈值设太低会出现大量误检,比如把地面上的斑马线当成托盘;设太高会漏检,尤其在逆光环境下label的置信度普遍偏低。项目里对label类别的检测阈值降到0.35,因为label是小目标且容易被光照影响;而rack是大目标,0.5以上才接受。这里的原则是不同类别用不同阈值,而不是全局一刀切。
3.3 货位映射:网格划分、中心点与交并比
检测到目标之后,核心问题是判断这个货箱属于哪个货位。项目采用的做法是:先通过rack检测框确定货架区域,再根据货架配置把区域划分成网格,网格的行对应层号,列对应列号。货物摆放比较整齐的仓库,用网格划分就够了;货架有倾斜或异形结构时,改用多边形区域判断。
位置判断不能只依赖检测框中心点。中心点在网格边界附近时,一个像素的抖动就可能跨格。项目里综合了两个指标:中心点是否落入目标货位区域,以及检测框与货位区域的交并比IoU。IoU高说明货物和货位重叠充分,结果可信;IoU低说明货物摆歪了或者跨了两个货位,要标记异常。
def map_to_slot(center_x, center_y, rack_region, grid_config): x_min, y_min, x_max, y_max = rack_region rows = grid_config['rows'] cols = grid_config['cols'] cell_w = (x_max - x_min) / cols cell_h = (y_max - y_min) / rows if not (x_min <= center_x <= x_max and y_min <= center_y <= y_max): return None col = int((center_x - x_min) / cell_w) + 1 row = int((center_y - y_min) / cell_h) + 1 return {'row': row, 'col': col}这段代码把货架区域按rows和cols两个参数均分成网格,根据中心点坐标算出行列号。row和col从1开始计数,符合仓库管理员的习惯。网格参数存在配置表里,不同货架的行列数不同,一个仓库可能有几十组配置。这里要注意:实际货架横梁有厚度,格子之间有间隙,严格的等分网格会把间隙也当作货位。更合理的做法是网格参数里增加gap_x和gap_y表示间隙占位,映射时先跳过间隙区域。
4. 避坑指南与排查实践:五个最容易翻车的地方
4.1 光照检测反而降低识别精度的坑
现象:用了CLAHE增强后,原本清晰的标签变得发灰,OCR识别率反而从90%掉到80%。原因:对正常曝光图片做局部对比度增强,会改变字符边缘的灰度分布,相当于人为引入了噪声。解决:先算灰度图平均亮度,低于阈值才执行增强;同时把增强后的图和原图都送进OCR,取置信度高的结果。从那以后我处理这类问题时,都默认先看直方图再决定要不要做增强。
4.2 透视校正后标签变糊的坑
现象:角点标注无误,但warpPerspective输出的标签区域字迹模糊。原因:标定区域过大,目标区域被拉伸到超过原始分辨率,插值算法补不出原本不存在的像素细节。解决:只对label检测框区域做透射校正,而不是对整个货架区域做。如果标签在校正后仍然偏小,先放大2到3倍再用双线性插值。放大倍数不是越大越好,超过4倍对清晰度几乎没帮助,只会增加耗时。
4.3 OCR把字母O识别成数字0的坑
现象:货位编号A01-R03-L02-C05里的字母O,被OCR识别成数字0,格式校验直接拦下这条记录。原因:PaddleOCR的字典里同时包含大写O和数字0,在低分辨率下两者形态极其相近。解决:在OCR后处理里加字符白名单,业务规则是货位编号首字母只允许A-Z,其他位置才允许数字。同时建立货位字典,对OCR结果做最近邻匹配,比如O01匹配到001时自动纠正并记录修正日志。这个纠正动作必须保留原始文本,方便事后追溯。系统设为review状态而不是直接confirmed。
4.4 检测框抖动导致货位跨格的坑
现象:连续视频帧中,同一货箱的检测框中心点在网格边界两侧来回跳动,产生两条相邻货位的识别记录。原因:单帧检测框有随机扰动,中心点坐标不可能绝对稳定。解决:项目里的多帧确认机制不是对检测框取平均,而是对货位编码投票——连续5帧中同一货位编码出现至少3次才写入确认结果。实现上可以用一个简单的计数器字典,每帧识别后累加计数,超过阈值就提交。
4.5 接口返回200但前端看不到数据的坑
现象:FastAPI接口调用成功,返回码200,响应体格式正确,但Streamlit页面一直显示空列表。原因:数据库连接串配置的是SQLite路径,服务启动时建了表,但前端服务读的是另一个环境变量,两个进程连到了不同数据库文件。解决:把数据库连接信息统一放到.env文件里,启动脚本里强制export,前后端服务共享同一份配置。日志里打印当前使用的数据库路径,第一次启动时用一条测试写入确认连通性。
5. 前后端联调与Streamlit复核界面:把识别结果用起来
5.1 统一响应结构与异常处理
后端接口设计遵循统一响应结构,格式是{"code": 0, "message": "success", "data": {...}}。code为0表示成功,非零表示业务异常。这里有个容易被新手忽略的设计点:HTTP状态码和业务状态码是两套体系。图像格式不支持时HTTP返回200但业务code返回40001,前端根据业务code渲染错误信息,而不是依赖HTTP状态码判断。这么做的好处是网关层不会因为业务错误重试请求,减少无效计算。
图片识别接口是核心。前端上传图片,后端保存到uploads目录,返回文件ID和识别任务ID。识别流程在后台异步执行,前端轮询任务状态。这个设计避免了同步接口超时的问题——一张1080p图片走完检测加识别可能需要3秒,浏览器默认超时是30秒,但并发高了以后排队时间不可控。
@app.post("/api/recognition/upload") async def upload_image(file: UploadFile = File(...)): if not file.content_type.startswith("image/"): return {"code": 40001, "message": "文件必须是图片格式", "data": None} ext = file.filename.split(".")[-1] if ext.lower() not in ["jpg", "jpeg", "png", "bmp"]: return {"code": 40002, "message": "不支持的图片格式", "data": None} task_id = str(uuid.uuid4()) save_path = f"uploads/{task_id}.{ext}" content = await file.read() with open(save_path, "wb") as f: f.write(content) background_tasks.add_task(run_recognition, task_id, save_path) return {"code": 0, "message": "上传成功", "data": {"task_id": task_id, "image_id": task_id}}文件类型校验这里用了两道过滤:MIME类型和扩展名。只校验扩展名会被人用改名的方式绕过,只校验MIME又会被某些编码格式干扰,两道校验都通过才允许落盘。文件保存后用uuid作为文件名,避免中文文件名和特殊字符带来的路径问题。异步任务用FastAPI的BackgroundTasks管理,进程重启时未完成任务会丢失,生产环境需要换成Celery或消息队列,单机验证阶段够用。
5.2 Streamlit复核界面与状态流转
复核界面在项目里扮演的是“人工兜底”角色。识别结果有三种状态:confirmed直接入库;review进入复核队列;failed直接丢弃。界面左侧是识别记录列表,右侧是原始图片和识别详情。管理员看到review状态的记录,可以手动确认或纠正货位编码,纠正后的结果写回数据库并更新确认时间。
import streamlit as st def render_review_page(conn): records = query_review_records(conn, limit=50) if not records: st.info("当前没有待复核的记录") return selected = st.selectbox("选择复核记录", [r["record_id"] for r in records]) record = next(r for r in records if r["record_id"] == selected) st.image(f"uploads/{record['image_path']}", caption="原始图片") st.write(f"OCR识别文本: {record['ocr_text']}") st.write(f"置信度: {record['confidence']:.2f}") new_slot = st.text_input("货位编码", value=record["slot_code"]) if st.button("确认入库"): update_status(conn, selected, "confirmed", new_slot) st.success("已确认并更新货位编码") if st.button("标记异常"): update_status(conn, selected, "failed", new_slot) st.warning("已标记为异常")Streamlit做内部工具界面比Flask加模板方式开发效率高很多,一个页面文件就是一个功能模块。这里需要注意selectbox的key冲突问题——多个selectbox组件用同一个label时Streamlit会报错,需要在参数里显式指定key。界面里展示的置信度是综合评分,计算方式是检测置信度、OCR置信度和多帧一致率的加权和,权重在配置文件里调整。
状态流转要保证幂等性。管理员的确认操作可能被重复提交,更新语句需要加上status='review'条件,如果更新影响行数为0说明记录已经被处理过。复核日志单独建表,记录操作人、操作时间、修改前后的货位编码。从项目实践来看,这个日志表是排查“谁改错了货位”的关键证据,不能省略。
整个系统跑通之后,我养成了一个习惯:每次部署新仓库都会用同一组测试图片在SQLite和MySQL两种数据库下各跑一遍,确认迁移脚本没有破坏字段映射。从那以后排查数据问题的时间少了一半。这套仓库货位识别系统的核心不在某一个模型的精度,而在于把图像处理、目标检测、OCR和业务规则串成一条可追踪、可干预的流程,希望这份拆解帮你在自己的项目里少踩几个坑。
本文还有配套的精品资源,点击获取