简介:面向计算机、嵌入式及人工智能相关专业学生的一份毕业设计文档,完整呈现基于树莓派的垃圾分类系统从架构设计到实验验证的全过程。该方案针对生活垃圾自动分类需求,采用树莓派主板作为核心,配合STM32驱动板、CSI摄像头、LED灯与LCD电阻屏,并借助Keras框架搭建卷积神经网络,选用LeNet-5网络结构完成垃圾图像识别,同时引入百度飞桨垃圾分类数据集进行训练与测试。包体为单个docx文档,压缩后102KB,内容涵盖系统总体框架、硬件选型、软件流程、模型构建、实验结果与参考文献等模块,便于直接查阅和二次修改。文档结合深度学习和图像识别技术,详细说明了垃圾分类模型的设计思路与关键参数,能够帮助读者快速理解边缘设备上的部署流程。已有891人学习,可为本专业毕业设计或创新项目提供较为完整的参考范例,尤其适合需要快速搭建原型或撰写设计报告的学习者。
1. 树莓派垃圾分类系统:先让模型稳定,再谈把垃圾桶接上树莓派
基于树莓派的垃圾分类系统,听起来像课程设计,真正做完才发现它是一个极其典型的硬件落地样本。树莓派 4B 的 CPU 跑 MobileNetV3 级别的图像分类模型,单帧推理能压在 200ms 以内,但加上摄像头抓拍、舵机转动和供电波动,整条投放流程很容易卡在 2 秒以上。我见过不少照着演示视频搭出来的项目,模型识别挺准,垃圾却进错桶——原因不是模型不好,而是数据一致性、硬件时序和机械节拍没对齐。这篇文章写给想从零搭一套“能稳定分四类”的桌面垃圾分类箱的人,也写给已经有树莓派、正在被这个项目折腾的从业者。先把模型、摄像头和舵机这三条线理清楚,后面真正落地才不会反复返工。
2. 硬件与执行机构选型:算力、摄像头、舵机怎么搭才不返工
先讲清楚选型。垃圾分类系统的硬件链路是:摄像头抓帧 → 树莓派推理 → GPIO 转舵机 → 垃圾落进对应格口。任何一环选错,后面都得返工,所以这里直接把选型理由讲透。
2.1 树莓派型号与推理后端:4B 为什么是甜点位,而不是最强款
树莓派 4B 是桌面垃圾分类场景的甜点位。树莓派 3B+ 跑同样的 TensorFlow Lite 模型虽然也能出结果,但舵机转动的瞬间 CPU 占用率会冲到接近满载,摄像头再一出帧,整条链路就开始卡顿;树莓派 5 性能确实更强,但它需要更高功率供电和主动散热,除非你想跑 YOLO 这类目标检测模型,否则在四分类场景里属于浪费。四分类任务本质是图像分类,单张 224x224 的输入就够了,树莓派 4B 的算力刚刚好落在性价比最合适的区间。
推理后端我建议用 TensorFlow Lite 官方运行时,配合 XNNPACK 算子加速。这个组合在树莓派 4B 的 Cortex-A72 四核上表现很稳,MobileNetV3Small 的单帧 CPU 推理大约在 100ms 到 200ms 之间,具体取决于输入尺寸和线程数。如果换成 ResNet50,单帧轻松超过 800ms,垃圾分类需要机械联动,推理太慢就失去了意义。线程数我一般设置成 2 到 4,线程太多反而会因为锁竞争导致速度下降,这是很多人容易踩的第一个性能坑。
模型在树莓派上的部署格式,我建议优先考虑 float16 量化,不行再降 int8。float16 精度几乎无损,树莓派上跑比 float32 快一截;int8 更快,但需要对校准数据集做标定,且最后几层容易出现精度损失。如果你用的是 2GB 内存版,建议直接对训练好的模型做 int8,省内存;4GB 版用 float16 完全够,系统也更好调试。
| 部件 | 推荐选型 | 理由 |
|---|---|---|
| 主板 | 树莓派4B(2GB/4GB) | 功耗与算力平衡,舵机联动时不至于满载 |
| 推理框架 | TensorFlow Lite + XNNPACK | 移动端 CPU 优化好,代码量小 |
| 分类网络 | MobileNetV3Small | 224x224 输入,算子兼容性最好 |
| 摄像头 | 免驱 USB 摄像头 | 驱动栈简单,固定曝光方便 |
| 舵机 | 演示用 SG90,日常用 MG996R | 按负载扭矩选,2.3 会给出计算方式 |
表格是个人常用配置参考。你如果手头已经有树莓派3B+,实验阶段也能跑,但别把舵机负载做太大,给 CPU 留出余量。树莓派 4B 的散热片一定要装,连续推理半小时后,温度能稳定在 60 度以下才是安全状态。
注意:如果你用的树莓派比较新,RPi.GPIO 可能装不上,需要换成 lgpio 或 gpiozero,PWM 接口参数类似,逻辑可以照搬。
2.2 摄像头与采集参数:固定曝光,比换一颗昂贵的摄像头更管用
垃圾分类系统翻车,很多不是模型不够准,而是摄像头每帧的亮度都不一样。树莓派默认的自动曝光会在光线稍微变暗时拉高 ISO,画面一变化,模型对训练集里没见过的新亮度就直接懵了。我自己的做法很简单:优先用免驱 USB 摄像头,用户态直接用 V4L2 读帧;CSI 摄像头画质确实更好,但 libcamera 的曝光参数调起来不如 V4L2 直观。如果灯光环境稳定,USB 摄像头完全够用。
对环境做一个参数固化:
# 用 V4L2 固定曝光和白平衡,避免每次抓帧时系统自动调节 v4l2-ctl --device=/dev/video0 --set-ctrl=auto_exposure=1 v4l2-ctl --device=/dev/video0 --set-ctrl=exposure_time_absolute=200 v4l2-ctl --device=/dev/video0 --set-ctrl=white_balance_automatic=0 v4l2-ctl --device=/dev/video0 --set-ctrl=white_balance_temperature=4500 ffmpeg -y -f v4l2 -i /dev/video0 -frames:v 1 sample.jpg命令里几个参数值得解释。exposure_time_absolute的单位是 0.1ms,200 就是 20ms 曝光,室内灯光下一般不会过亮,也不会拖影;white_balance_temperature=4500把色温固定到室内灯常用的值,防止白纸拍出来一会儿偏蓝一会儿偏黄。固定完之后,模型在部署环境里看到的输入永远是同一色温和亮度,跟训练集保持一致。如果换到别的房间,先抓一帧 sample.jpg 看亮度,再微调曝光时间就行。
还需要注意一个问题:摄像头的安装高度和角度。我一般让摄像头垂直俯拍投料口,让垃圾尽量在画面中央占满一半以上。不要斜着拍,斜视角度会让物体轮廓变形,影响分类结果。如果有条件,在投料口上方加一个小 LED 灯,用恒亮照明替代环境光抖动,效果立竿见影。
2.3 执行机构方案与舵机扭矩估算:SG90 不是万能的
机械部分,桌面垃圾分类箱最常见的结构是单舵机翻板:垃圾从投料口落在斜板上,舵机带动翻板转到对应的方向,垃圾滑入格口。还有一种旋转分隔盘方案,舵机驱动圆盘转到对应位置再接住垃圾。我推荐翻板方案,舵机只需 0 到 90 度摆动,动作行程短,速度快,结构也简单,3D 打印或五金的加工量都小。
舵机选型不是闭眼买 SG90。SG90 标称扭矩约 1.8kg·cm,只够带一块轻薄的塑料翻板。如果你要投递一盒 300g 的剩饭,翻板力臂按 8cm 算,需求扭矩大约是 0.3kg × 8cm = 2.4kg·cm,SG90 很快就过热抖动。我一般按这个方式估算:力矩 = 负载质量 × 力臂长度,再留 1.5 倍余量。演示级项目随便用 SG90,想每天用、扔得动食物残渣和饮料瓶,至少得用 MG996R 这个级别的金属舵机。
电源设计是另一个返工重灾区。舵机启动瞬间的电流能达到 1A 到 2A,如果直接用树莓派的 5V 引脚供电,电压会被瞬间拉低,轻则舵机抖动,重则树莓派直接重启。常见做法是外接独立 5V 降压模块给舵机供电,再把电源地和树莓派地线连在一起,这样 PWM 信号才有统一的参考点。我见过有人把舵机正负极直接并到树莓派 40Pin 的 5V 与 GND 上,表面省事,一启动就翻车,别这么干。
2.4 垃圾箱结构与投料口设计:机械限位比程序限位更可靠
结构上,投料口和格口之间的距离要留够,防止翻板还没转到位置,垃圾已经滑出去。机械上最好加一个物理限位柱,把舵机臂的行程限制在安全角度内,而不是完全靠程序控制。程序一旦跑飞,物理限位能挡住舵机臂,避免舵机堵转烧毁。箱体外壳用 3D 打印或亚克力板都行,关键是摄像头支架要独立,不能跟舵机外壳装同一个振动面,否则舵机转动时的振动会传到图像上。
3. 训练垃圾分类模型并部署到树莓派:从 Keras 到 TFLite 的最小命令
硬件和机械的坑讲完了,下一步是模型侧。分类模型的训练和部署,核心是:用 PC 做迁移学习,再把模型压缩成 tflite 丢到树莓派。重点不是训一个世界第一的模型,而是做一个在固定环境下足够稳、体积小、推理快的模型。
3.1 训练集怎么组织:四分类场景里最容易偷懒也最容易翻车的地方
垃圾分类四分类,我按常见标准拆成可回收物、厨余垃圾、有害垃圾、其他垃圾。训练集每类至少准备 200 到 300 张图,类别之间数量尽量均衡。不要直接把一段视频逐帧抽出来训练,因为相邻帧几乎一样,模型会过拟合到这个物体的样子,而不是这类垃圾的共性。
收集数据时要在同一个部署环境下拍。比如把摄像头安装在箱体内,用固定曝光拍各类垃圾。训练集里的物体角度,应该覆盖正上方、偏左、偏右和少量遮挡。如果你只在正上方拍,模型对侧面进来的物体几乎无力应对。数据增强是必须的:轻度旋转、水平翻转、亮度抖动。但注意不要做剧烈的对比度变化和裁剪,否则会让模型学到太强的伪影。
如果公开数据集只能补足一部分,我的建议是人工补拍,而不是加太多网络图片。网络图片背景太杂,会失真且引入大量视觉噪声。四分类不是大工程,每类补够 300 张,训练一个迁移模型完全够用。数据路径组织为 train 和 val 两个目录,子目录名就是类别名,方便后续代码读取。
3.2 在 PC 上训练:迁移学习从 Keras 一行代码开始
下面给出一个可以照着跑的完整训练脚本:
import tensorflow as tf from tensorflow.keras import models, layers IMG_SIZE = 224 BATCH = 16 # 从目录读图,label_mode="categorical" 表示输出为 4 维 one-hot 标签 train_ds = tf.keras.preprocessing.image_dataset_from_directory( "./dataset/train", label_mode="categorical", image_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH, ) val_ds = tf.keras.preprocessing.image_dataset_from_directory( "./dataset/val", label_mode="categorical", image_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH, ) # 用 ImageNet 预训练权重,冻结主干,只训练新分类头 base = tf.keras.applications.MobileNetV3Small( input_shape=(IMG_SIZE, IMG_SIZE, 3), include_top=False, weights="imagenet", ) base.trainable = False x = layers.GlobalAveragePooling2D()(base.output) x = layers.Dropout(0.3)(x) out = layers.Dense(4, activation="softmax")(x) model = models.Model(inputs=base.input, outputs=out) model.compile( optimizer=tf.keras.optimizers.Adam(1e-3), loss="categorical_crossentropy", metrics=["accuracy"], ) model.fit(train_ds, validation_data=val_ds, epochs=10) model.save("garbage_classifier.h5")参数说明:IMG_SIZE=224与 MobileNetV3Small 默认输入尺寸一致;BATCH=16是普通 PC 上也能稳跑的值。冻结主干意味着这几层权重在训练中不变,只有最后的分类头在学。epochs=10 先看趋势,不要一上来就训 50 轮,避免过拟合。Dropout(0.3)会让模型对训练集收敛慢一些,推理时自动不生效。
如果 10 轮训练精度在 90% 以上,就继续;如果不到 90%,先检查数据质量。一个常见误用是标签是 one-hot,却配了sparse_categorical_crossentropy,这两者不匹配会报错。我这个脚本已经用 one-hot 标签配合categorical_crossentropy,照抄即可。
3.3 转换成 TFLite:float16 还是 int8?
训练完成后,关键一步是把 h5 转成 tflite,并做量化。下面先给一个 float16 的转换:
import tensorflow as tf model = tf.keras.models.load_model("garbage_classifier.h5") converter = tf.lite.TFLiteConverter.from_keras_model(model) # Optimize.DEFAULT 配合下面的 target_spec,产出 float16 量化模型 converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_types = [tf.float16] converter.target_spec.supported_ops = [ tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS, ] tflite_model = converter.convert() open("garbage_classifier_fp16.tflite", "wb").write(tflite_model)supported_ops加上SELECT_TF_OPS是防止某些自定义层转换失败,代价是 tflite 运行时需要包含兼容算子。如果你的网络全是标准卷积和池化,不加问题也不大。float16 转换不需要校准数据,这是它最大的优势。转换完的模型大小大约是 float32 的一半,树莓派内存也省。
如果还是觉得慢,再上 int8:
def representative_gen(): import numpy as np images = load_calibration_images() # 从 val 集读 100 张,并 resize 到 224x224 for img in images: yield [np.expand_dims(img, 0).astype(np.float32)] converter2 = tf.lite.TFLiteConverter.from_keras_model(model) converter2.optimizations = [tf.lite.Optimize.DEFAULT] converter2.representative_dataset = representative_gen tflite_model = converter2.convert() open("garbage_classifier_int8.tflite", "wb").write(tflite_model)int8 标定需要代表性数据,load_calibration_images()是你自己写的读图函数,从校验集随机选 100 张。输入必须是 float32 类型,但转换器内部会统计数值范围,量化成 uint8 输入。注意:如果 int8 模型上线后某一类精度明显下降,请回到 float16 或 float32,优先保证稳定而不是追求极致速度。
3.4 在树莓派上跑推理:输入输出对齐,别把模型当黑匣子
最后一步在树莓派上写推理脚本。树莓派环境不建议装完整 TensorFlow,太重,装 tflite-runtime 就能跑:
import numpy as np import tflite_runtime.interpreter as tflite from PIL import Image interp = tflite.Interpreter(model_path="garbage_classifier_fp16.tflite") interp.allocate_tensors() inp = interp.get_input_details()[0] out = interp.get_output_details()[0] # 打印输入输出 dtype,方便后面对齐格式 print("input dtype:", inp["dtype"], "shape:", inp["shape"]) print("output dtype:", out["dtype"], "shape:", out["shape"]) def predict(image_path): img = Image.open(image_path).convert("RGB").resize((224, 224)) arr = np.asarray(img, dtype=np.float32) / 255.0 arr = np.expand_dims(arr, axis=0) interp.set_tensor(inp["index"], arr) interp.invoke() scores = interp.get_tensor(out["index"])[0] return scores.argmax(), float(scores.max())代码里的关键点是dtype对齐。float16 转换后的模型,输入通常是 float32,所以用dtype=np.float32;如果换成 int8 量化模型,输入 dtype 会是 uint8,需要把arr变成 0~255 的 uint8 并删除归一化步骤。拿到类别和置信度后,下一步就是传给 GPIO 去控制舵机,进入机械联动环节。
4. 舵机与传感器联动:让垃圾桶真的按类别开合
模型调通之后,系统只完成了“眼睛”。接下来的部分要解决核心问题:分类结果怎么变成一个可靠的机械动作。
4.1 用 RPi.GPIO 输出 PWM:角度映射与释放
树莓派标准做法是用 RPi.GPIO 库输出 50Hz 的 PWM 信号。50Hz 意味着周期 20ms,舵机通过 PWM 高电平时间也就是脉宽来确定角度。常见舵机 0 度对应 0.5ms 到 1ms 脉宽,180 度对应 2.5ms 脉宽。下面代码直接给出一个角度映射:
import RPi.GPIO as GPIO import time SERVO_PIN = 18 GPIO.setmode(GPIO.BCM) GPIO.setup(SERVO_PIN, GPIO.OUT) pwm = GPIO.PWM(SERVO_PIN, 50) # 50Hz,20ms 周期 pwm.start(0) def set_angle(angle): # 按 0.5ms~2.5ms 脉宽近似映射为 0~180 度 duty = angle / 180.0 * 10.0 + 2.5 pwm.ChangeDutyCycle(duty) time.sleep(0.3) # 给舵机机械动作留时间 pwm.ChangeDutyCycle(0) # 释放占空比,防止舵机一直发抖关键在最后一行:pwm.ChangeDutyCycle(0)是很多人会漏掉的。舵机转到目标角度后,如果不把占空比清掉,PWM 会持续给出目标脉宽,舵机会因为一直修正位置而抖动、发烫。正确做法是转到位后释放,让舵机靠机械自锁停在那个角度。time.sleep(0.3)根据舵机实际速度调整,SG90 大约 0.12s/60 度,0.3 秒足够完成 90 度动作,又不至于拖沓。
如果你的舵机对 50Hz 不敏感、出现转不到指定角度或者缓慢抖动,先检查信号线是否接对,再确认舵机型号支持的 PWM 频率。有些数字舵机需要更高频率,但绝大多数模拟舵机在 50Hz 下都能正常工作。PWM 频率设置错误属于比较隐蔽的坑,表现起来像是舵机坏了,其实是频率不对。
4.2 主循环逻辑:识别、翻转、复位三段式
主循环不应该让树莓派一直跑推理,那是徒耗 CPU。我做的是“触发式”流程:先用接近传感器检测到有垃圾投入,再拍一帧、做推理、转舵机。下面是一个简化版结构:
import time CLASS_NAMES = ["可回收物", "厨余垃圾", "有害垃圾", "其他垃圾"] ANGLE_MAP = [45, 90, 135, 0] # 每个类别对应的舵机角度,按机械结构修改 def handle_garbage(): # 触发后先抓一帧,再做推理 cat, conf = predict("latest_frame.jpg") if conf < 0.6: category = 3 # 低置信度时投到其他垃圾 else: category = cat set_angle(ANGLE_MAP[category]) time.sleep(1.5) # 等待垃圾滑落 set_angle(60) # 复位到默认待机角度 while True: if detect_object(): # 接近传感器检测到投料 capture() # 调用 ffmpeg 或 libcamera 抓一帧 handle_garbage() time.sleep(0.2)这套流程的核心是“低置信度兜底”。模型不可能任何时候都自信,低于 0.6 就别硬分,投到其他垃圾是垃圾分类里最稳妥的兜底策略,避免把可回收物错扔进厨余。ANGLE_MAP 不是乱写的,每一类对应一个机械格口角度,这个角度需要在机械安装后实际标定。主循环里的time.sleep(0.2)是让出 CPU,防止不断读传感器导致树莓派过热降频。
抓帧函数capture()可以直接调用 ffmpeg 命令行,不建议在 Python 里反复初始化摄像头对象,那样每次抓帧可能有 0.5 秒的初始化延迟。我一般用一个常驻的 ffmpeg 进程输出到文件,或者直接用 picamera2 的连续回调做节流,尽量减少每次抓帧的开销。
如果在一轮里连续投入多个垃圾,我的处理是每投入一个就完整走一遍“识别→翻转→复位”,不要在同一个舵机角度上连续执行两次动作。否则第二次动作时上一块垃圾可能还卡在格口边缘,导致夹料。必要的话在代码里加一个busy状态变量,处理完一个垃圾桶才允许触发下一次投料。
提示:低置信度丢进其他垃圾是垃圾分类系统的常用兜底策略,不要为了追求“每次都有明确分类”而把阈值调低,误分的代价比不分类更大。
4.3 接近传感器的经验:防空转,还省电
接近传感器常见的方案是 HCSR04 超声模块,便宜、简单,对塑料瓶和纸盒的检测都还行。接线方面,Trig 接 GPIO23,Echo 接 GPIO24。需要注意的是,Echo 输出的是 5V 电平,树莓派 GPIO 只能承受 3.3V,所以中间必须加一个电阻分压,不然可能烧掉树莓派的 GPIO 引脚。下面是可以直接跑的代码:
import RPi.GPIO as GPIO import time TRIG = 23 ECHO = 24 GPIO.setmode(GPIO.BCM) GPIO.setup(TRIG, GPIO.OUT) GPIO.setup(ECHO, GPIO.IN) def detect_object(): GPIO.output(TRIG, True) time.sleep(0.00002) GPIO.output(TRIG, False) # 等待 echo 电平跳变,记录脉冲时间 while GPIO.input(ECHO) == 0: pulse_start = time.time() while GPIO.input(ECHO) == 1: pulse_end = time.time() distance = (pulse_end - pulse_start) * 34300 / 2 # 音速约 343m/s return distance < 10 # 10cm 以内认为有物体这个函数有一个隐患:如果没有物体,或者模块接线有问题,while GPIO.input(ECHO) == 0会一直等下去,把整个主循环卡死。常规做法是加超时判断,比如开始计时后超过 0.1s 没等到回波就返回 False。另外,超声模块对软质布类垃圾反射效果差,如果测试发现经常漏检,可以换成红外避障传感器,检测距离调到 5cm,抗环境光更好。
加传感器不只是防空转,还有很现实的省电意义。如果主循环保持高频推理,树莓派温度升高后频率会下降,整个系统反而更慢。改成接近传感器触发后,空闲时系统负载很低,风扇转速下降,使用体验会稳很多。
5. 树莓派垃圾分类系统常见问题:四个让人返工的排查现场
这一部分是我在实际搭系统时最想保留的排查记录,每一条都是踩过的坑,按“现象、原因、解决”写清楚。
5.1 现象:推理延迟很高,画面像幻灯片
把摄像头预览打开,画面一卡一卡的,拍一次要等很久。查了进程,树莓派 CPU 占用 100%,但单帧推理却要 1 秒以上。原因有两个:一是很多教程里的 tflite 解释器默认线程数只有 1,没有利用四核 CPU;二是代码里每帧都用Image.open()重新解码 JPEG,解码本身就占了大量 CPU。
解决方式,给解释器设置多线程,并把 JPEG 解码交给更高效的方式:
interp = tflite.Interpreter(model_path="garbage_classifier_fp16.tflite") # 显式设置线程数为 4,充分利用四核 interp.set_num_threads(4) interp.allocate_tensors()解码那一层,我在后面的代码里改用 opencv 读取,并先把摄像头分辨率压到 640x480。224x224 的分类输入不需要 1080p 原图,高分辨率纯属浪费 CPU。调整之后推理时间能降到 150ms 左右,系统整体温度也降下来,这是一个立竿见影的优化。
5.2 现象:舵机转到一半抖动,几秒后树莓派重启
现象很明确:每次执行翻板动作,舵机都是先抖两下,然后整机重启,日志里能看到欠压报错。原因也很直接:舵机瞬间电流把树莓派 5V 总线电压拉低,触发了树莓派自己的欠压保护。这和 2.3 节强调的电源问题完全对应。
解决是把舵机供电彻底移出树莓派。我常做的方案是:外接一个 DC-DC 5V 降压模块,输入用 12V 电源适配器,输出接舵机电源线;树莓派自身用单独的 5V 3A 适配器;最后把两路电源的 GND 接到一起。舵机供电线上还可以并联一个 470uF 的电解电容,缓解启动浪涌。改完之后,舵机本体仍然会发热,但不再抖,系统也不再重启。
5.3 现象:模型把带包装的可回收物全部分到其他垃圾
把装有包装袋的可回收物放到投料口,系统明明拍到了,却分到其他垃圾。排查思路不是模型不够强,而是训练集和部署环境的差异。我的训练集在摄像头垂直拍摄、灯光充足的环境下拍,垃圾桶却放在窗边,下午逆光,画面偏暗还带投影。模型从未见过这种光照条件,置信度就很低。
解决分两步。第一步是物理条件对齐:在垃圾桶内部加辅助灯,让投料口的光照恒定,并在代码里继续用 2.2 节的固定曝光参数。第二步是数据补强:对训练集做亮度抖动增强,让模型见过更暗的输入。我重新训练后,同一瓶包装袋,置信度从 0.55 提到 0.82。这说明很多时候不是模型翻车,是数据与场景不匹配。
5.4 现象:systemd 服务开机不启动,手动执行又能跑
开机后看不到任何相关进程,手动执行 Python 脚本却一切正常。这个坑在树莓派项目里非常典型。原因是 systemd 服务的环境变量与用户会话不同,脚本里用了相对路径,或者依赖某个环境变量,开机时拿不到;还有一个可能是脚本里用了 GPIO,而服务默认用户是 root,某些 GPIO 库在 root 环境下反而有问题。
解决的惯用做法,是给服务指定用户和绝对路径,并推迟到系统完全启动后再拉起:
[Unit] Description=Garbage Classification Sorter After=multi-user.target [Service] ExecStart=/usr/bin/python3 /home/pi/sorter/run_sorter.py WorkingDirectory=/home/pi/sorter Restart=on-failure User=pi [Install] WantedBy=multi-user.targetWorkingDirectory一定要写,避免脚本内部相对路径踩坑;User=pi使用普通用户跑 GPIO,比 root 更贴近手动执行时的权限环境;Restart=on-failure保证脚本异常退出后自动拉起。改完记得执行sudo systemctl daemon-reload再启用服务。上线前先systemctl start手动启动一次,看日志确认跑通,再启用开机自启。
6. 把系统调到日常可用:置信度、曝光和舵机角度标定
到这里,模型、机械、电源都通了,但“能跑”和“能日常用”之间还差三步标定。第一步是置信度阈值。不要死守 0.6,我的做法是先在真实环境下抓 20 张图跑一遍,看模型输出的分数分布。如果某一类大多数情况都在 0.75 以上,可以把阈值提高到 0.7;如果各别类别普遍偏低,降到 0.5 也行。千万不能定到 0.9,那样会变成所有东西都不置信,每个垃圾都被丢进其他垃圾,等于白做。反过来说,阈值太低会误分,我的经验是 0.5 到 0.7 之间最容易找到甜点。
第二步是曝光微调。代码里固定了exposure_time_absolute=200,但不同房间的灯亮度不同。抓一帧 sample.jpg,如果画面偏暗就把曝光值往上调,比如 300;如果偏亮就往下调。注意每次改完都要重新抓帧确认,不要用上次的照片做依据。白平衡的 4500 是室内 LED 常用值,如果你用的是暖黄光,调成 3500 更合适;偏冷的白光,5000 也常见。不要随便动白平衡,除非画面明显偏色。
第三步是舵机角度标定。这一步最容易“差一点就翻车”。我给每个格口对应的舵机角度做一次校准:
# 标定脚本:依次让舵机转到几个角度,观察垃圾是否准确落入对应格口 import time for angle in [0, 45, 90, 135, 180]: set_angle(angle) print("当前角度:", angle) time.sleep(2)跑这个脚本时,你会看到舵机臂在不同角度上是否与格口对齐。如果偏了,修改 ANGLE_MAP 里的数值,比改机械结构快得多。注意转轴固定螺丝要拧紧,不然转动几十次后角度会漂移,又得重新标定。最后,做个简单的验收测试:每类垃圾连续投 5 次,要求正确率达到 4/5 以上,才算具备日常可用的底线。
我自己的教训是:最开始把全部精力花在提高模型准确率上,忽略了机械结构标定,结果模型 95% 的准确率被舵机 5 度的安装偏差浪费,真正分对率只有六成。后来把摄像头、舵机、光照这三样先固定死,再回头调模型参数,系统才算真正落地。这个顺序你下次一定先记住。希望帮到你。
本文还有配套的精品资源,点击获取