
简介这是一份围绕小程序物体识别开发的完整工程包面向希望从前端到后端贯通深度学习的开发者覆盖微信小程序、腾讯云轻量应用服务器、Django接口服务与YOLOv3模型部署等环节。压缩包内共3832个文件约414.62MB以JavaScript、TypeScript脚本和JSON配置为主并包含Node.js依赖、Markdown说明文档、YAML配置及YOLOv3模型压缩包可帮助搭建从数据准备到模型调用的完整链路。目前已有482人学习下载。通过对照目录中的源码、依赖声明和配置项读者可理解小程序调用后端API、Django集成YOLOv3推理、腾讯云环境配置等关键技术同时资源附带微信开发者工具安装程序与模型备份便于本地复现和二次开发也适合作为毕业设计或课程项目的参考资料。尤其适合初学者对照实践快速掌握前后端联调与模型落地的流程。1. 物体识别小程序不是跑模型而是跑服务架构有个开发者同学第一次拿到这套“小程序物体识别开发腾讯云轻量云服务器搭建”压缩包时先把里面所有模型文件翻了个遍以为把检测模型接进小程序页面就完事了。结果真正卡了他三个晚上的是服务器端那一整套链路模型推理服务、HTTPS 域名、上传接口、小程序白名单。今天这篇笔记就围绕这套资源展开讲清楚为什么物体识别不能直接塞进小程序包以及怎么用腾讯云轻量云服务器把识别能力变成可调用接口。适合正在做识别类课程设计、毕设 Demo 或工具型小程序的开发者前端后端都不熟也没有关系资源包里每一步都有可以直接抄的代码。2. 物体识别方案选型模型放手机还是放服务器差距不只是体积2.1 端上推理的四个硬边界很多人第一眼看到“物体识别小程序”第一反应是找个移动端推理引擎把 YOLOv5s 转成 TFLite塞进小程序包。这个思路看起来合理模型才 14MB手机上跑个模型有什么难的但真动手你就会碰到第一个坎小程序包体限制。普通小程序主包不得超过 2MB整个小程序总体积也有上限14MB 的权重文件意味着你连测试版本都发不出去。有人会说可以拆分包分包同样算资源体积而且用户体验会多一步等待加载。第二个坎是推理框架。模型文件不是能独立运行的东西需要配套推理引擎。TFLite 版本的 YOLO 在小程序端要能跑通常还得转成 JavaScript 版本或依赖 WebAssembly转换过程常遇到算子不支持的问题最后要么降级模型版本要么手写算子。这个工程量已经超过了你希望花在这个功能上的时间预算。即便硬装上了冷启动时加载 40MB 推理产物和模型在低端安卓机上要 2 到 3 秒用户打开页面直接看到一个白屏。第三个坎是算力和内存。端侧 CPU 推理一张 640x640 的图中低端机型普遍 1.5 秒以上识别过程中内存峰值能到几百 MB部分低内存机型上小程序页面会被系统直接回收表现为页面突然闪退。用户拍一张照片等三秒出结果结果下一秒小程序没了体验是灾难级的。云端服务器推理同样的模型单张耗时大约 300ms内存峰值不到 1GB差距是数量级。第四个坎是模型迭代。端上模型一旦要升级需要重新发布小程序版本走一遍审核流程。云端方案直接替换服务器上的 ONNX 权重文件重启服务就生效前端代码一行不用动。这四个坎层层递进任何一个都足以让端上方案夭折所以资源包选择把模型放到服务器上是一个成熟的工程判断不是图省事。2.2 云端方案的调用链路与密钥边界资源包给出的云端链路是这样的小程序端调用系统相册接口选择图片拿到临时文件路径后通过上传接口发到你的服务器服务器收到图片先做尺寸校验和压缩然后交给提前加载好的模型进行推理最后把结果拼成 JSON 返回。小程序端解析 JSON用 Canvas 把每个检测框绘制在图片上。整条链路里小程序只做“采集图片”和“绘制结果”所有计算都发生在服务器。这里有一个特别容易被忽略的安全设计密钥必须留在服务器。如果直接让小程序调用云厂商的识别 API需要把 SecretId、SecretKey 写在小程序代码里。小程序包一旦发布任何人都能反编译或者抓包拿到密钥轻则被别人刷掉你的配额重则密钥泄露后影响账号下其他资源。资源包的做法是在轻量服务器上自建一层接口把云厂商密钥放在服务端环境变量里小程序只知道你的服务器域名。这层代理还顺手解决了跨域、限流和日志记录的问题后续加缓存和鉴权也都在同一个位置做。2.3 选型对比表与模型规格的取舍对比项端上推理云端API 轻量服务器包体影响14MB模型框架转完40MB起步小程序包体几乎无变化首帧延迟低端机可超过3秒2核服务器300ms左右密钥安全被迫硬编码易泄露密钥只留服务端模型更新重新发版走审核替换服务器权重文件离线可用可以不行服务器成本无低配即可表格里要补充一条离线场景是端上方案的保留地比如拍照识别需要完全离线使用那只能吃模型压缩的苦。但是对面向课程设计、毕设答辩、工具类小程序而言评审和真实用户都处于有网络环境云端方案的稳定性明显更好。模型规格方面资源包选的是 YOLOv5s而不是更大的 v5m 或者更新的 v8 系列。原因不在精度的绝对值在于 ONNX 导出和后处理脚本的成熟度。v5s 的算子导出路径清晰社区里找得到的 NMS 代码和坐标映射代码基本可以无缝复用v8 系列也一样好但对于一份教学落地资源来说选最大公约数版本遇到部署问题的可能性最小。如果你业务里小目标很多可以换 v5m 或 v8s但你需要接受单张推理时间从 300ms 涨到 800ms 左右。3. 腾讯云轻量应用服务器搭建把 YOLOv5s 变成可调用接口3.1 服务器配置选择与基础环境先说配置。别一上来就买高配机器2 核 2G 的轻量应用服务器带宽 3M 到 5M就足够跑 YOLOv5s ONNX 的 CPU 推理。它比同规格的 CVM 便宜而且自带固定公网 IP流量套餐对小型应用来说绰绰有余。这套资源的使用场景是接口调用不是模型训练所以不需要 GPU。系统选 Ubuntu 22.04 或 Debian 12新系统上装最新版 Python 依赖最省心避免老系统 glibc 版本不足导致 onnxruntime 装不上或一跑就崩。装完系统后第一时间更新软件源并装必要依赖apt update apt install -y python3-pip nginx pip3 install fastapi uvicorn onnxruntime opencv-python-headless numpy这里特意用 opencv-python-headless 而不是 opencv-python因为服务器上不需要 GUI 支持headless 版体积小也不会在无桌面环境时报 GTK 错误。onnxruntime 装 CPU 版即可轻量服务器没有 NVIDIA 显卡GPU 版装了也用不上。如果你后续自己训练模型换权重再补装 PyTorch 也不迟但推理服务本身不依赖它。注意所有命令都在 root 或 sudo 用户下执行建议单独建一个 www-data 系统用户跑服务不要直接用 root 起 uvicorn。3.2 服务端识别接口与模型加载资源包里会带一个转好的 YOLOv5s.onnx 模型和完整的 main.py。自己写也没问题核心代码就两块模型推理和 HTTP 接口。模型加载以模块级别做一次避免每个请求都重新初始化import cv2 import numpy as np import onnxruntime as ort # 全局只初始化一次 session冷启动代价只承担一遍 session ort.InferenceSession( yolov5s.onnx, providers[CPUExecutionProvider], sess_optionsort.SessionOptions() ) session.set_providers([CPUExecutionProvider]) # 限制运行时线程数2核机器上多线程反而拖慢速度 session.intra_op_num_threads 2参数里 CONF_THRESHOLD 控制置信度0.25 是 COCO 预训练模型的常用下限实际使用时如果误检多就抬到 0.35漏检多就降到 0.15。IOU_THRESHOLD 用于 NMS越低去重越狠但重叠物体容易丢掉一个框。INPUT_SIZE 固定 640这也是 YOLOv5s 的标准输入边长改小能提速但小目标漏检会变严重。推理前的预处理要严格按 letterbox 来做。直接 resize 会把原始图像目标比例拉变形尤其是窄长条图片最后画框时坐标会全部偏移def letterbox(img, new_shape(640, 640), color(114, 114, 114)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw, dh (new_shape[1] - new_unpad[0]) / 2, (new_shape[0] - new_unpad[1]) / 2 top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) # 用灰色常量填充边缘而不是纯黑色 img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return imgletterbox 最后返回的是带灰边的正方形图这才是模型的输入。推理输出是一个形状为 (1, 25200, 85) 的数组25200 是 640x640 下三个尺度预测框的总数85 是 80 类目标加 5 个框属性。这一步如果不做后处理直接解析 25200 行数据你会看到成百上千个重叠框。YOLOv5 的后处理主要是置信度过滤加 NMS。FastAPI 接口部分接收上传的文件转成 OpenCV 图像返回 JSONfrom fastapi import FastAPI, UploadFile, File app FastAPI() app.post(/detect) async def detect(file: UploadFile File(...)): image_bytes await file.read() img_array np.frombuffer(image_bytes, np.uint8) img cv2.imdecode(img_array, cv2.IMREAD_COLOR) # BGR 格式 # BGR 转 RGB并归一化到 0-1 input_img letterbox(img) input_tensor input_img[:, :, ::-1].transpose(2, 0, 1)[None] input_tensor input_tensor.astype(np.float32) / 255.0 outputs session.run(None, {images: input_tensor})[0][0] objects postprocess(outputs, img.shape[1], img.shape[0]) return {objects: objects}这里[:, :, ::-1]是将 OpenCV 的 BGR 转成 YOLO 需要的 RGBtranspose 再交换成 CHW 布局归一化除以 255。为什么不能省很多入门的人直接喂 BGR 图结果模型把每个类都识别成背景因为训练时喂的是 RGB。这个小细节能解释你常见的“什么也识别不出来”的问题。postprocess 函数里要做一个关键步骤把 letterbox 后的坐标映射回原图。否则画框会往右下角偏一条边距def postprocess(outputs, orig_w, orig_h): boxes outputs[:, :4] confs outputs[:, 4] # 判断是否有目标 class_scores outputs[:, 5:] class_ids class_scores.argmax(axis1) final_scores confs * class_scores.max(axis1) mask final_scores 0.25 # 置信度过滤 ... keep nms(boxes[mask], final_scores[mask], 0.45) # 关键把 letterbox 坐标映射回原图 # 假设 r、dw、dh 已从 letterbox 取出 boxes_unpad (boxes - [dw, dh, dw, dh]) / r映射公式是(x - pad_w) / rr 和 pad 都来自 letterbox 的返回值。资源包里这段处理是封装好的但你自己调试时务必确认这一步存在否则所有框位置都是错的。3.3 Nginx 反向代理与 HTTPS 证书配置FastAPI 的 uvicorn 默认跑在 8000 端口小程序正式版不会允许你请求http://x.x.x.x:8000。小程序要求所有请求域名必须 HTTPS并且域名需要在小程序管理后台配置白名单。把 Nginx 摆在前面做反向代理顺便终结 TLS 证书server { listen 443 ssl; server_name your-domain.example; # 证书链必须完整缺中间证书会导致真机校验失败 ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; # 手机原图动辄 3-8MB默认 1m 必被拒 client_max_body_size 10m; location /detect { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 识别服务冷启动时响应慢给足 60 秒 proxy_read_timeout 60s; } }client_max_body_size 设置成 10m是为了避免压缩后的图片仍然较大时被 Nginx 直接拒绝。proxy_read_timeout 设 60 秒保证模型第一次冷启动时不会因为等待响应超时被 Nginx 掐断。证书可以用免费的 ACME 客户端申请也可以用云厂商的免费证书。注意申请完一定要补全证书链很多云厂商下载的 cer 文件和 key 文件配对后Nginx 启动不报错但小程序真机会告诉你“非合法域名”。3.4 用 systemd 守护识别服务轻量服务器重启后uvicorn 进程不会自己起来这是新手最容易忽略的一环。手动启动服务SSH 一断进程就没了。用 systemd 把它变成系统服务[Unit] DescriptionYOLO Detect API Afternetwork.target [Service] ExecStart/usr/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2 Restartalways Userwww-data WorkingDirectory/opt/yolo-api [Install] WantedBymulti-user.targetRestartalways 是保命配置进程意外退出后 systemd 会在两秒内重新拉起来。User 换成 www-data 是为了不给 Python 进程 root 权限这是服务器安全的基础习惯。workers 设 2不要设太高2 核机器上 worker 多了反而会因为线程切换把 CPU 耗尽。写完后执行systemctl daemon-reload和systemctl enable --now yolo-api服务就会伴随开机自启。验证接口是否通了用 curl 传一张测试图curl -X POST https://your-domain.example/detect \ -F filetest.jpg返回的 JSON 里如果包含了 objects 数组链路就是通的。这一步做完服务器端的活就到此为止剩下小程序的对接。4. 小程序端对接从选图到 Canvas 画框的完整链路4.1 图片选择与压缩策略小程序端代码主要落在两个文件里一个负责选图上传一个负责渲染结果。选图用框架自带的媒体选择接口它对用户来说是最自然的入口wx.chooseMedia({ count: 1, mediaType: [image], sizeType: [compressed], // 直接拿系统压缩后的图 success(res) { const tempFilePath res.tempFiles[0].tempFilePath uploadImage(tempFilePath) } })sizeType 设为 compressed会在选图阶段就拿到系统压缩后的图片体积通常能压到 500KB 以内。这个参数对低带宽的轻量服务器非常重要因为 3M 带宽的上行速度只有 384KB/s原图 8MB 上传要二十多秒压缩后只需两秒。注意 count 不要开大一次识别一张多选反而让代码复杂。如果 compressed 出来的图仍然很大可以在服务器端再增加一道压缩逻辑。常见做法是 OpenCV 按比例缩到最长边 1280px再以 JPEG 质量 80 编码。这一步对识别精度几乎没有影响因为模型输入只有 640px1280 的边长已经完全够用。自己动手实战时可以在 FastAPI 接口里加一段压缩逻辑同时把压缩后的图片保存一份到临时目录方便事后排查画框是否偏移。4.2 上传文件与统一响应结构上传用 wx.uploadFile注意 name 字段必须和服务器端 FastAPI 接口里 UploadFile 的参数名一致否则服务器收不到文件function uploadImage(filePath) { wx.uploadFile({ url: https://your-domain.example/detect, filePath: filePath, name: file, // 与后端参数名保持一致 timeout: 30000, success(res) { // res.data 是字符串必须先 JSON.parse const result JSON.parse(res.data) if (result.objects.length 0) { wx.showToast({ title: 未识别到物体, icon: none }) return } drawPrediction(filePath, result.objects) }, fail(err) { wx.showToast({ title: 识别失败请重试, icon: none }) } }) }timeout 设 30000 毫秒给服务器端冷启动留足余地。识别结果里 objects 是空数组时说明模型置信度没有超过设定的阈值这时不要走画框逻辑直接给用户一个提示。这里有个容易踩的坑小程序的 wx.uploadFile 返回的 res.data 是字符串必须先 JSON.parse 才能取字段。如果你把 res.data 直接当对象用取出来的值全是 undefined页面表现为识别不到任何结果。这种问题日志里不会有任何报错玄学味道特别重。建议服务端把响应结构固定成{code: 0, objects: [...]}这样小程序端可以先判断 code再做后续渲染排查问题时一眼能看清是业务问题还是网络问题。4.3 Canvas 2D 绘制检测框与标签画框推荐用 Canvas 2D 节点。先要在 wxml 里放下一个 canvas 标签并给一个固定尺寸之后图片和框都画在这块画布上const query wx.createSelectorQuery() query.select(#canvas).fields({ node: true, size: true }).exec((res) { const canvas res[0].node const ctx canvas.getContext(2d) const img canvas.createImage() img.onload () { // 先画底图再叠加识别框 ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight) drawObjects(ctx, result.objects) } img.src tempFilePath })drawObjects 里四个坐标必须按识别的图片尺寸等比换算。假设识别时图片大小是 info.width 和 info.heightCanvas 显示宽度是 canvasWidth缩放比就是 canvasWidth / info.widthfunction drawObjects(ctx, objects, infoWidth, canvasWidth) { const ratio canvasWidth / infoWidth objects.forEach((item) { // 框坐标用像素值直接从服务端返回原图像素坐标 const x item.box[0] * ratio const y item.box[1] * ratio const w (item.box[2] - item.box[0]) * ratio const h (item.box[3] - item.box[1]) * ratio ctx.strokeStyle #00FF00 ctx.lineWidth 3 ctx.strokeRect(x, y, w, h) ctx.fillStyle #00FF00 ctx.fillText( ${item.label} ${(item.confidence * 100).toFixed(1)}%, x, y - 6 ) }) }常见翻车是把服务器返回的归一化坐标当像素坐标用画出来的框比图还大。建议在服务端直接返回原图像素坐标小程序端拿到后只做等比缩放这一件事前端逻辑越简单越不容易错。这里还要注意一条canvas 的宽度最好用图片实际显示宽度不要用屏幕宽度否则小图会被放大导致框错位。连续识别时重绘前先调用ctx.clearRect(0, 0, canvasWidth, canvasHeight)否则上一次的框会残留在画布上看起来像识别出了很多重复目标。4.4 真机预览与本地调试的三个前置条件在小程序开发者工具里跑通接口后真机预览往往会再翻一次车。首先开发者工具默认不校验合法域名模拟器里能请求不代表真机能请求必须在小程序后台把https://your-domain.example加入 request 合法域名。其次真机上传图片时微信会对非 HTTPS 请求直接拦截所以不要用 IP 加端口的形式调试把 Nginx 和证书配好之后再上真机。最后开发者工具里的 Canvas 渲染结果和真机可能有差异尤其是字体大小和线宽建议真机截图确认框的位置是否贴合识别目标。5. 避坑指南轻量服务器上跑识别服务的五条血泪经验5.1 现象上传图片报 413服务端日志却干干净净配合 Nginx 部署后小程序端一传图就报 413查看 FastAPI 日志没有任何报错因为请求根本没到达服务端。原因Nginx 默认的 client_max_body_size 是 1m手机照片轻松超过这个上限服务器直接在 Nginx 层就拒绝了。解决在 server 块里设置client_max_body_size 10m甚至 20m改完 executenginx -s reload。从那以后我每次部署接口服务第一件事永远是先搜一遍配置文件里的 body size别再被这道隐形门卡住。5.2 现象浏览器访问域名正常小程序真机却提示「不在合法域名列表」原因有两个层面。第一个是域名没有在小程序后台配置 request 合法域名需要把https://your-domain.example加进白名单。第二个是证书链不完整浏览器会自动补全中间证书小程序却严格要求证书链完整缺一环都会拒绝连接。解决申请证书时选择完整链文件 fullchain.pem用openssl s_client -connect your-domain.example:443验证证书链确认返回的证书链包含中间证书后再检查小程序后台白名单。这个问题浪费过我一个下午浏览器、电脑端开发者工具全正常真机一测就“非合法域名”。5.3 现象识别置信度很高画出的框却整体偏移服务器返回框坐标小程序在图上画出来发现框的位置偏左上或右下。原因图片在服务端做 letterbox 时填充了灰色边框坐标在 letterboxed 坐标系内没有反算回原始坐标系就直接返回了。解决在后处理里把 x、y 减去偏移量 dw、dh再除以缩放比例 r返回的坐标才是原图中的真实位置。验证方法把服务端返回的坐标和原图叠加用 OpenCV 保存到服务器上人工看一眼框是否贴住目标。这一步是资源包后处理代码里最容易改错的地方每次替换模型都要重新验证一遍。5.4 现象单张识别很快并发一多服务器就卡到几乎无响应2 核 2G 的轻量服务器单张推理 300ms但并发超过 5 个请求服务器就假死。原因onnxruntime 默认会启用部分 CPU 优化多线程推理在 2 核机器上线程切换开销巨大加上 uvicorn 单 worker 模式没有并发能力。解决把推断器初始化时的intra_op_num_threads设为 2限制线程数uvicorn 启动参数加--workers 2让请求排队而不是同时抢占 CPU。资源包的部署脚本里通常会带这两个参数自己写的时候千万别漏。5.5 现象模型换新文件后识别结果和之前完全一样明明是替换了模型权重重新加载后结果却不变化。原因onnxruntime 的 session 是模块级全局变量服务进程还存活时覆盖磁盘上的模型文件不会影响已经加载进内存的 session。解决加载前把模型文件先拷到临时文件校验 md5 后再替换文件名重启 systemd 服务再用 curl 验证输出是否变化。这个坑在第一次线上换模型时翻过车排查了快两小时最后发现只是忘了重启。6. 进阶把单张识别延迟压到 500ms 以内的三个手段6.1 服务器端二次压缩不要在高清原图上直接做 letterbox。先用 OpenCV 把最长边缩到 1280再按质量 80 编码 JPEG缩放后图片更小解码和 resize 都更快。这一步在大图上经常能省 100ms。注意别把质量降到 50 以下JPEG 压缩产生的块效应会影响小目标的检测。6.2 结果缓存与常用物体预热识别请求往往会反复提交相同图片尤其是测试阶段。在内存里维护一个 key-value 缓存Key 用图片内容的 md5Value 是最后的识别结果。命中缓存直接返回省掉整个推理流程。缓存容量设置 200 条足够每条存储几个对象信息内存开销几乎为零。如果某些物体在你的场景里高频率出现可以在服务启动时用几张代表性图片预热 session前几个请求就不会再经历冷启动。6.3 并发生命周期管理轻量服务器的带宽是共享的3M 带宽的并发瓶颈往往不在 CPU而在带宽。上传请求是串行还是并行决定了体感速度。建议把小程序端上传改为“单张请求、失败重试、不乱发并发请求”的模式服务器端把 uvicorn worker 数量固定为 2配合 Nginx 做平均负载。你可以在服务端写一个简单的计数中间件把每个请求的耗时打点超过 800ms 的请求单独观察。从那以后我每次部署云端识别服务都会先在服务器上跑一轮 10 并发压测脚本再拿着压测时打印的耗时分布去调参数。这轮压测能同时暴露线程数配置、带宽上限和模型推理瓶颈比上线后被用户骂一遍要省心得多。这套资源包里的服务端代码、小程序页面和部署脚本都齐全把教程里提到的配置和踩坑点过一遍你也能把物体识别从 Demo 状态推到可上线状态。希望帮到你。本文还有配套的精品资源点击获取