ARTICLE DETAIL

资讯详情

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

基于YOLOv11的车牌识别系统Web前端设计与实战解析

基于YOLOv11的车牌识别系统Web前端设计与实战解析 1. 项目整体架构与UI前端设计思路1.1 UI前端在整个车牌识别项目里到底负责什么先把这个系列的前因后果捋一遍。前面几篇我们已经完成了YOLOv11车牌检测模型的训练、车牌字符识别模块的封装也设计好了识别结果的存储方案。到了第三篇核心任务很明确把这套能力做成一目了然的操作界面。说直白点就是给系统装一张脸让用户不碰代码也能用起来。很多刚开始做这类项目的朋友容易陷入一个误区觉得UI就是画个页面、放两个按钮等模型调通以后随便套个Web框架就行。真上手才发现前端页面和模型推理服务之间的衔接才是大头。这一篇我会把我们实际项目中踩过的坑、调过的参数、改过三遍的设计方案都讲清楚尤其是在视频流处理、接口并发、浏览器兼容这几个地方很多细节不实际跑一遍很难意识到。整个UI系统的职责可以拆成三块操作入口、结果展示、状态反馈。操作入口负责接收用户上传的图片、发起识别请求结果展示负责把检测框、车牌号、置信度这些信息直观地反馈出来状态反馈解决的是系统正在处理中和处理失败这类过程信息看似不起眼但在实际使用中体验差异巨大。1.2 为什么选Web前端方案而不是桌面应用我们在方案选型阶段其实纠结过一阵。PyQt5做桌面工具自由度很高但一旦涉及跨平台打包、摄像头预览、远程访问这些需求复杂度就上来了。Streamlit这类快速演示工具写起来确实快但交互细节很难精细控制真实项目里很少直接用。最终我们选了Flask加原生HTML/JavaScript的技术栈页面框架用了Bootstrap 5做基础样式。核心原因有三个。第一前后端彻底分离推理服务独立运行以后要做移动端或者小程序接口可以原封不动地复用。第二浏览器天然支持视频流播放MJPEG视频流推给前端只需要一个img标签这在车牌识别的实时监控场景里是极刚需的能力。第三开发调试效率高前端模板改完刷新页面就能看到效果不用重新编译打包。从部署角度看Web方案还有一个隐藏优势识别服务跑在服务端模型权重和Python环境全部在服务器上客户端零安装。对于停车场道闸、出入口监控这类场景现场只需要一台能开浏览器的设备就能操作运维成本低很多。提示如果项目只在本机单用户使用PyQt5也是可以接受的方案。但从长期演进和团队协作角度考虑Web方案的可维护性明显更高这也是我们最终坚持Web路线的原因。1.3 页面功能模块与交互流程设计页面功能我们按照使用场景拆成了三个标签页。第一个是图片识别页用户可以上传单张图片系统返回标注了车牌位置的图片和识别出的车牌文本。第二个是实时视频页打开摄像头之后画面实时显示在浏览器中检测到的车牌区域会自动框选出来同时页面上展示当前画面内识别到的车牌号。第三个是识别记录页以表格形式展示历史识别记录包含识别时间、车牌号、置信度和对应的缩略图。这三个模块对应了三种典型使用方式单张快速查验、现场实时监控、事后追溯查询。实际项目中停车场入口的抓拍识别是图片识别逻辑监控大屏用的是实时视频逻辑管理员查询车辆进出记录用的是记录列表逻辑。UI设计一开始就按场景拆后面接业务逻辑会很顺手。交互流程上图片识别页是典型的上传后识别流程实时视频页是流式展示加异步结果刷新记录页则是拉取数据后渲染列表。这三个流程的通信方式和数据格式各不相同这是我接下来要重点说的内容。2. 前后端通信与接口设计2.1 HTTP轮询还是WebSocket这个选择很关键很多人在做实时识别页面时第一反应是上WebSocket觉得双向通信才是现代方案。实际做下来在这个项目里WebSocket并不是必需的甚至可以说MJPEG视频流加HTTP轮询的组合更合适。MJPEG流实现的原理是后端不断向响应流中写入以boundary分隔的JPEG图片帧前端通过img标签的src直接指向视频流地址浏览器会自动刷新显示最新帧。这种方案的优点是实现简单直观代码量极少而且前端的视频帧和检测框天然同步——因为框就是在帧上画好了再推过来的不存在前端自己对齐坐标的麻烦。那识别出来的车牌文本怎么传给前端呢我的做法是后端在每帧检测完成后把结果写入一个全局的最近结果变量前端用一个几百毫秒的定时器轮询获取。为什么不用WebSocket推呢因为车牌识别并不是高频推送场景结果变化频率很低轮询的代码量和调试成本都更低也不会被浏览器防火墙或代理拦截。当然如果场景上升到需要双向实时交互、多客户端推送、消息队列级别的架构那WebSocket是更好的选择。但对于这个项目阶段HTTP轮询完全够用而且稳定性有保障。2.2 核心API设计与参数约定接口设计我列了一张表按照图片识别、视频流、结果查询三类来划分接口方法用途关键参数返回内容/api/detect_imagePOST图片上传识别multipart表单file字段JSON含车牌号、置信度、标注图路径/video_feedGET实时视频流无MJPEG流每帧为标注后的JPEG图/api/latest_resultGET获取最近一次识别结果无JSON含车牌号、置信度、检测时间/api/recordsGET获取历史记录page, page_sizeJSON记录列表和分页信息/api/camera/onPOST打开摄像头识别无JSON表示启动成功与否/api/camera/offPOST关闭摄像头识别无JSON表示关闭成功与否这里有一个设计细节值得强调图片识别接口返回的是标注图片的路径而不是图片的base64字符串。路径方式可以让前端直接用img标签加载减小JSON体积同时方便浏览器做缓存。为了管理这些临时图片后端会定时清理result目录中超过一定时间的文件避免磁盘占用无限膨胀。统一返回格式对前端异常处理非常重要。我们约定所有接口返回的JSON结构都是下面这样{ code: 0, message: success, data: { } }code为0表示成功非0表示业务异常message携带错误描述。前端全局封装一个fetch函数先判断code再决定走成功逻辑还是弹错误提示这样所有接口的错误处理逻辑可以复用不会出现一个接口一个写法的情况。2.3 识别结果的数据结构设计车牌识别结果看起来简单但在数据结构上还是要设计清楚否则后面接记录列表和统计功能会很痛苦。我们定义的data字段结构是{ plate_text: 京A12345, confidence: 0.92, plate_bbox: [120, 300, 340, 380], image_path: /static/results/20250220_143052.jpg, plate_image_path: /static/results/20250220_143052_plate.jpg, timestamp: 2025-02-20 14:30:52 }plate_text是最终识别出的车牌字符串confidence是车牌检测的置信度plate_bbox是原图中车牌的像素坐标框image_path是标注后的整图路径plate_image_path是裁剪出来的车牌特写图timestamp是识别时间。plate_image_path这个字段一开始我们没设计后来发现非常有用。因为车牌字符识别偶尔会出错保留车牌区域的特写图可以让管理员快速核验识别得对不对而不需要盯着整张图找车牌在哪里。实际使用中这个特写图也是历史记录页缩略图的数据来源。3. 实操从零实现UI前端页面3.1 环境准备与项目目录结构动手写代码之前先搭好项目骨架。完整的目录结构是这样的plate_project/ ├── app.py # Flask主应用路由和页面渲染 ├── detector.py # YOLOv11推理封装 ├── ocr_module.py # 车牌字符识别模块 ├── camera.py # 摄像头帧采集与预处理器 ├── database.py # SQLite操作封装 ├── requirements.txt # 依赖清单 ├── models/ # 模型权重目录 │ └── plate_detect.pt # YOLOv11训练好的车牌检测权重 ├── templates/ │ └── index.html # 主页面模板 ├── static/ │ ├── css/ │ │ └── style.css # 自定义样式 │ ├── js/ │ │ └── main.js # 前端交互逻辑 │ ├── vendor/ # 本地化的Bootstrap和jQuery │ └── results/ # 标注结果图片存放目录 └── plates.db # SQLite数据库文件环境依赖方面核心是ultralytics库、Flask和OpenCV。我的requirements.txt一般是ultralytics8.3.x flask3.0.x opencv-python4.9.x numpy安装直接用pip搞定。有一个点要注意ultralytics库版本更新频繁不同版本之间的API有细微差异建议把版本锁定住。我们项目里最开始用的是8.2.x后来升级到8.3版本后predict返回的results对象有些字段变了导致前端展示逻辑报错排查了很久。固定版本是最稳妥的做法。3.2 后端推理服务封装推理解耦这一步很关键。把YOLOv11的加载和推理逻辑单独封装成PlateDetector类避免在Flask路由里直接操作模型。这样做的好处是模型加载一次后常驻内存每次识别请求直接调用预测方法不重复加载权重否则每次请求都要等几秒钟加载模型体验非常差。核心代码写出来是这样的import cv2 import numpy as np from ultralytics import YOLO class PlateDetector: def __init__(self, weightsmodels/plate_detect.pt, conf_thres0.45): self.model YOLO(weights) self.conf_thres conf_thres def detect(self, frame): 输入: BGR图像numpy数组 返回: 检测框列表 [(x1, y1, x2, y2, confidence)] results self.model.predict(frame, confself.conf_thres, verboseFalse) boxes [] if len(results) 0: return boxes for r in results[0].boxes.data.tolist(): x1, y1, x2, y2, score r[0], r[1], r[2], r[3], r[4] boxes.append((int(x1), int(y1), int(x2), int(y2), float(score))) return boxes实际项目中我建议把模型的预热也放在初始化阶段。第一次predict之前先对一张纯黑图跑一次前向推理把模型内部的一些初始化操作提前完成避免第一个真实请求响应特别慢。这个预热过程只需要几十毫秒但对用户体验提升很明显。车牌检测完不算完事字符识别模块要吃掉检测框里的图像区域。ocr_module.py里我封装了一个识别函数输入是检测框坐标和整帧图像输出是车牌字符串。这里用的是传统图像处理加模板匹配的方式没有接大模型因为车牌字符集合是固定的模板匹配方案在车牌特写图质量好的情况下识别率足够而且CPU上跑得快不依赖GPU。3.3 图片识别页面的前端实现图片识别页面的逻辑相对简单核心就是一个表单上传加结果回显。HTML部分主要是一个文件选择框、一个上传按钮、一个结果展示区。div classcard div classcard-header图片车牌识别/div div classcard-body input typefile idimageInput acceptimage/* button classbtn btn-primary onclickuploadImage()开始识别/button div idresultArea classmt-3 img idresultImage classimg-fluid alt p idresultText/p /div /div /div前端JavaScript使用fetch发送FormData逻辑很直接async function uploadImage() { const fileInput document.getElementById(imageInput); if (!fileInput.files[0]) { alert(请先选择图片); return; } const formData new FormData(); formData.append(file, fileInput.files[0]); const resp await fetch(/api/detect_image, { method: POST, body: formData }); const result await resp.json(); if (result.code 0) { document.getElementById(resultImage).src result.data.image_path ?t Date.now(); document.getElementById(resultText).textContent 车牌号: result.data.plate_text 置信度: Math.round(result.data.confidence * 100) %; } else { alert(识别失败: result.message); } }有个细节必须说明src的img标签后面拼接了时间戳参数这是为了解决浏览器缓存问题。同一张图片文件如果路径不变浏览器会直接用缓存的旧图导致第二次上传同名文件时显示的还是第一次的识别结果。加上时间戳强制刷新这个坑就避开了。后端的接口处理也贴出来app.route(/api/detect_image, methods[POST]) def detect_image(): f request.files.get(file) if f is None or f.filename : return jsonify({code: 1, message: 未接收到图片文件}) img_bytes f.read() img cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) if img is None: return jsonify({code: 1, message: 图片解码失败}) boxes detector.detect(img) if len(boxes) 0: return jsonify({code: 1, message: 未检测到车牌}) x1, y1, x2, y2, score boxes[0] plate_img img[y1:y2, x1:x2] plate_text ocr_module.recognize(plate_img) filename datetime.now().strftime(%Y%m%d_%H%M%S_%f) .jpg annotated img.copy() cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 255, 0), 2) # 这里用PIL画中文车牌文本 annotated draw_chinese_text(annotated, plate_text, (x1, y1 - 15)) cv2.imwrite(os.path.join(STATIC_DIR, results, filename), annotated) cv2.imwrite(os.path.join(STATIC_DIR, results, plate_ filename), plate_img) return jsonify({ code: 0, data: { plate_text: plate_text, confidence: score, image_path: /static/results/ filename, } })这里要特别提醒一个很多人都会遇到的坑OpenCV的cv2.putText不支持中文直接画车牌号上去会显示成乱码。我的处理方案是借助PIL库先把OpenCV的BGR格式转成PIL的RGB格式用ImageDraw绘制中文文本再转回OpenCV格式。这个方法不复杂后面排障章节我会把代码贴全因为这是所有中文标注场景的通用痛点。3.4 实时视频识别页面的实现实时视频页面是整个前端部分技术含量最高的地方核心是video_feed接口返回的MJPEG流。前端只需要一个img标签src指向后端的流地址。后端生成视频流的代码是这样的def generate_frames(): camera Camera() while True: frame camera.get_frame() if frame is None: continue # 检测 boxes detector.detect(frame) if len(boxes) 0: x1, y1, x2, y2, score boxes[0] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) plate_img frame[y1:y2, x1:x2] plate_text ocr_module.recognize(plate_img) update_latest_result(plate_text, score) # 写入全局变量 ret, buffer cv2.imencode(.jpg, frame, [cv2.IMWRITE_JPEG_QUALITY, 80]) if not ret: continue frame_bytes buffer.tobytes() yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n frame_bytes b\r\n) app.route(/video_feed) def video_feed(): return Response(generate_frames(), mimetypemultipart/x-mixed-replace; boundaryframe)前端页面上的核心元素img idliveVideo src/video_feed width960 height540 alt视频流 p idlivePlateText classlive-plate-text/pJS轮询setInterval(async () { const resp await fetch(/api/latest_result); const result await resp.json(); if (result.code 0 result.data.plate_text) { document.getElementById(livePlateText).textContent 识别到车牌: result.data.plate_text; } }, 800);这个方案的实际效果很直观浏览器里实时画面流畅刷新车牌框跟着车走下方文本区域会更新识别到的车牌号。实现成本很低但对车位识别演示、监控复核这样的场景已经非常实用。需要注意MJPEG流的帧率控制很重要。默认情况下摄像头读取帧的速率取决于摄像头本身的FPS如果摄像头是30帧后端每帧都要跑一次YOLOv11推理CPU或GPU压力很大而且画面延迟并不会因为跑满30帧而变低。我的做法是控制生成器循环里的帧率在两次帧读取之间sleep上一段时间把最终推流的帧率控制在10到15帧左右这个帧率对识别场景足够资源占用也能接受。3.5 识别记录页面的实现记录页本质上是一个数据表格页面核心是后端从SQLite读取识别记录返回给前端渲染。数据库表结构很简单CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, plate_text TEXT NOT NULL, confidence REAL NOT NULL, image_path TEXT, plate_image_path TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );每次成功识别一条车牌就把结果写入数据库这既能支撑记录页的数据展示也为后续做车辆进出统计打下基础。前端用fetch拉数据渲染表格的代码块如下async function loadRecords(page 1) { const resp await fetch(/api/records?page${page}page_size20); const result await resp.json(); if (result.code ! 0) return; const tbody document.getElementById(recordsTableBody); tbody.innerHTML ; result.data.records.forEach(item { const row document.createElement(tr); row.innerHTML td${item.created_at}/td td${item.plate_text}/td td${Math.round(item.confidence * 100)}%/td tdimg src${item.plate_image_path} width120 classimg-thumbnail/td ; tbody.appendChild(row); }); // 更新分页信息 }记录页我加了一个分页参数这个看起来是小事但实际使用中记录增长很快一次性拉全量数据会导致页面渲染卡顿而且浏览器内存占用也会变大。分页后每页20条配合简单的上一页下一页按钮整个页面的加载速度稳定在毫秒级别。4. 常见问题与排查实录4.1 视频流卡顿和延迟的优化记录视频流卡顿基本上是做实时识别页面最容易遇到的问题而且原因往往不止一个。我们在开发中就碰到过视频画面每隔几秒就卡一下、锅铲大小、画面和声音不同步这类现象。第一个要排查的是Flask开发服务器的并发模型。Flask自带的app.run默认是单线程的视频流接口长时间占用一个worker其他接口全部排队导致页面轮询识别结果时超时给人的感觉就是整个页面卡住了。解决方式是启动时加上threadedTrue参数app.run(host0.0.0.0, port5000, threadedTrue)第二个原因是推理耗时太长。如果每秒钟要处理30帧而每帧推理时间是100毫秒那么处理能力只有10帧大量帧堆积在队列里延迟自然越来越大。我的处理思路是主动降帧在摄像头读取循环里显式加延迟控制在每秒12帧左右让系统有足够的余量去处理每帧反而画面更流畅因为不会出现积压。第三个容易被忽略的是图片尺寸。摄像头原始图像可能是个200万像素的大图直接送入YOLOv11推理显存和耗时都会很高。我在摄像头初始化时就把分辨率设置为960x540这个尺寸对于车牌识别足够了。既降低了推理耗时也减少了视频流传输的带宽一举两得。4.2 跨域与图片文件访问障碍跨域问题是在前后端分离部署时才会遇到如果我们直接用一个Flask服务同时提供页面和接口浏览器不会报CORS错误因为同源。但有很多读者喜欢把前端页面放在Vite开发服务器或者Nginx里和后端接口分开跑这时候跨域问题就来了。解决方案有两个。第一个是后端加Flask-CORS库允许指定来源跨域访问开发阶段最省事。第二个是把前端的API请求全部走相对路径再由Nginx做反向代理把/api路径转发到后端服务。生产环境我推荐第二种不仅解决跨域还能统一处理HTTPS、负载均衡、静态资源缓存。还有一个非常常见的问题页面能打开但识别结果图片加载不出来浏览器控制台报404。排查下来大多是Flask静态目录配置错了比如图片存到了项目根目录下的static/results而前端请求的路径是/static/results/xxx.jpgFlask默认会把static文件夹映射到/static路径。如果目录结构不对就要在Flask初始化时显式指定static_folder。4.3 推理接口超时和并发冲突图片上传接口在实际使用中会遇到一个隐蔽问题大尺寸手机照片上传后接口处理时间可能超过浏览器默认的超时时间。前端fetch默认不会主动中断但如果用户上传的是12MB的原始照片从图片解码到YOLOv11推理再到返回耗时可能长达数秒配合弱网环境用户会觉得点按钮没反应。我们在这里做了双保险。第一是后端限制上传文件大小和分辨率超过5MB的图片先压缩到1280像素以内再送入推理。车牌识别并不需要原图全分辨率清晰度满足车牌字符可辨识即可压缩后速度和精度都不会受影响。第二是前端上传时显示loading状态并且设置一个较长的超时避免用户因为等待误以为系统崩溃。并发冲突方面如果多个用户同时调用识别接口YOLOv11模型对象是被多个线程共享的。ultralytics内部有线程安全机制但高并发下GPU显存申请会出现竞争。我们的处理方式是加一个线程锁确保同一时刻只有一个推理任务在执行import threading infer_lock threading.Lock() def safe_detect(frame): with infer_lock: return detector.detect(frame)这样做的代价是并发请求会排队但对于一个道闸单通道场景同时间根本不会有太多请求排队等待反而比并发竞争更稳定。4.4 中文标注乱码问题的完整解法这个问题我单独拿出来说因为太典型了。YOLOv11检测框可以正常画但把京A12345这串中文标注到图片顶部时用OpenCV自带的putText画出来就是一堆问号。原因是OpenCV的putText只支持内置的Hershey字体这个字体并不包含中文字符。我的解决方法是把画文本的工作交给PILfrom PIL import Image, ImageDraw, ImageFont def draw_chinese_text(img, text, position): pil_img Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) draw ImageDraw.Draw(pil_img) font ImageFont.truetype(simhei.ttf, size20) draw.text(position, text, fontfont, fill(0, 255, 0)) return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)这里要注意两点字体文件路径请确保服务器上存在Windows自带simhei.ttfLinux服务器则需要单独安装中文字体包否则会报文件找不到的错误每次调用都加载字体文件很低效最好在模块初始化时把字体对象创建好复用。4.5 摄像头资源泄漏与释放摄像头资源管理在开发时容易踩坑尤其在Windows系统上反复打开关闭摄像头几次之后后面再打开就会报设备被占用。排查发现是前一次视频流关闭时摄像头对象没有正确释放。我在Camera类中增加了显式的close方法并且利用Python的with语句和try/finally保证异常情况下也能释放资源class Camera: def __init__(self, index0): self.cap cv2.VideoCapture(index) self.cap.set(3, 960) self.cap.set(4, 540) def get_frame(self): ret, frame self.cap.read() return frame if ret else None def close(self): if self.cap.isOpened(): self.cap.release()同时在后端路由中注册了关闭事件当客户端断开视频流连接时生成器通过GeneratorExit异常触发清理逻辑把摄像头释放掉这样下次再打开就不会有占用问题。问题现象可能原因解决方案页面卡住、其他接口超时Flask默认单线程app.run(threadedTrue)视频画面越来越延迟帧率过高推理帧积压控制输出帧率10~15FPS中文车牌显示为问号OpenCV不支持中文用PIL绘制中文文本图片上传后无响应图片过大处理超时压缩图片后再推理摄像头第二次打不开上一轮资源未释放close()释放摄像头5. 性能优化与部署经验5.1 推理接口异步化的实战取舍同步接口在演示demo阶段足够但从真实项目角度看一个识别请求如果耗时1到2秒前端按钮长时间处于等待状态用户体验很差。我们可以把识别任务改造成异步模式提交任务后立即返回task_id前端轮询任务完成状态完成后拿结果渲染。实现方式不需要引入消息队列那么重的依赖Python自带线程池就能解决from concurrent.futures import ThreadPoolExecutor executor ThreadPoolExecutor(max_workers2) app.route(/api/detect_image_async, methods[POST]) def detect_image_async(): # 保存上传图片到临时文件 task executor.submit(process_image_task, tmp_path) task_id str(uuid.uuid4()) tasks[task_id] task return jsonify({code: 0, data: {task_id: task_id}}) app.route(/api/task_result/task_id) def task_result(task_id): task tasks.get(task_id) if task is None: return jsonify({code: 1, message: 任务不存在}) if not task.done(): return jsonify({code: 0, data: {status: running}}) return jsonify({code: 0, data: {status: done, result: task.result()}})前端轮询的时候间隔控制在1秒左右等任务完成后展示结果。这个优化在真实的停车场道闸场景中非常重要因为抓拍识别和抬杆放行是联动操作服务端不能因为一个请求繁忙就把所有请求都卡住。5.2 前端页面加载速度与资源管理前端性能优化的核心是不要依赖公网CDN。很多教程让直接用Bootstrap CDN链接内网部署时一旦断网页面全部裸奔。我们的做法是把Bootstrap、jQuery、字体文件全部下载到本地static/vendor目录页面引用本地路径。另外识别结果图片如果直接返回原图每次请求都要从磁盘读到内存再通过HTTP传输图片越大响应越慢。静态资源方面可以启用Flask的send_from_directory配合浏览器缓存对于动态生成的图片则可以生成固定尺寸的缩略图。实践发现车牌特写图展示用200像素宽完全足够缩略图方案让历史记录页的加载速度提升了不止一个量级。5.3 生产环境部署细节开发环境用Flask自带服务器跑得再稳到生产环境就必须换掉。我的经验是用waitress一个纯Python的WSGI服务器Windows和Linux都支持配置简单。部署启动命令是waitress-serve --listen0.0.0.0:8000 --threads4 app:app如果是Linux服务器或者系统里装好了Nginx可以考虑gunicorn加gevent worker的组合并发能力更强。Nginx反向代理配好之后还可以顺手把静态资源的缓存策略、Gzip压缩一并做了整体性能会有明显提升。开机自启方面Windows环境用任务计划程序或者nssm把服务注册成系统服务Linux环境用systemd写一个service文件。这些细节看着琐碎但正是工程化落地和demo之间的分水岭。注意正式生产环境一定不要把Flask的debug模式开着expires和缓存头的配合也要谨慎设置避免识别结果图片被无限缓存导致展示旧图。这里再分享一个我们在实际部署时遇到的小问题服务放在一台只有CPU的服务器上YOLOv11推理速度比较慢。后来我们把模型导出为OpenVINO格式CPU推理速度提升了将近一倍。如果你也遇到CPU推理慢的瓶颈可以往这个方向研究一下改动很小收益却很直接——把ultralytics加载的模型文件后缀从.pt换成OpenVINO导出的文件夹路径即可。最后说一点个人体会。车牌识别项目做到UI这一环很多人以为是最轻松的实际上恰恰相反。前端和推理服务之间隔着网络、并发、格式、跨域、资源管理这些最后一公里的问题任何一个细节没处理好整体体验都会大打折扣。我做过好几个类似的识别项目最大的感悟就是接口设计要早于页面开发数据格式的约定要足够规范摄像头和线程资源要一开始就考虑释放策略而不是等项目跑通了再回头补。这套经验我在做道闸抓拍、安防监控、车牌识别管理后台的时候反复用了很多次如果你现在也在做类似方向希望这篇实战系列对你有所帮助。
返回列表