news 2026/10/1 12:46:53

YOLO驾驶员疲劳检测模型实战:从数据集构建到PERCLOS告警

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLO驾驶员疲劳检测模型实战:从数据集构建到PERCLOS告警

简介:面向计算机视觉与智能驾驶安全领域的研究者、开发者,该资料包含基于YOLO算法的驾驶员疲劳检测完整模型与配套数据集,可识别驾驶员闭眼、打哈欠等疲劳行为,适用于疲劳驾驶预警系统研发、算法课程教学、毕业论文或课题验证。压缩包共2000个文件,涵盖1984个txt标注、13个md说明文档、2个pdf技术文档及1个yaml模型配置文件;其中txt标注可直接用于YOLO系列模型训练,yaml定义网络结构,md与pdf可辅助理解算法原理、使用流程与可视化效果,包体整体约306MB。目前已有1139人学习下载。数据集特意将标注按txt与xml两种格式分别归档于两个文件夹,提供双格式训练素材,省去标准转换步骤;配合项目说明文档和可视化参考,用户可快速完成环境配置、模型训练与效果评估,并进一步部署到实际驾驶监测场景。适合希望快速上手驾驶员疲劳检测的中初级视觉学习者,也为进阶调优提供良好基线。

1. 用 YOLO 算法做驾驶员疲劳检测模型,最该先想清楚的是数据而不是网络

夜里两点,挂着挂车的司机在高速匝道上闭眼超过三秒——车载摄像头拍到了,但没人看回放。这正是 yolo算法驾驶员疲劳检测模型+数据集 这类项目要解决的典型场景:不依赖方向盘传感器、不依赖心率带,只靠一个朝驾驶员方向的内视摄像头,实时判断人是不是困了。做这件事的人通常是三类:ADAS 供应商的算法工程师、做车队安全管理系统的开发者、准备把这个题目当毕设或参赛课题的学生。我的建议是先别纠结 YOLOv8 还是 YOLOv5,先把“疲劳”定义成模型能监督的标签,再把数据集按场景补齐。后面每一章都是一个能直接复现的节点,从信号定义、模型选型、数据集构建一路做到部署避坑。

2. 疲劳信号建模:为什么盯眼睛最可靠,PERCLOS 和 EAR 怎么落到 YOLO 上

疲劳检测模型并不是在检测“困”这个抽象状态,而是在检测一组肉眼可观察、可量化的表现:眼睛闭合时间变长、打哈欠频率上升、头部不自觉地低头。把这三种现象落到模型输出上,就是三类完全不同的监督任务。

2.1 三种可选信号通道:闭眼、哈欠、点头的稳定性与落地成本对比

信号模型需要输出判定逻辑稳定性主要坑
闭眼眼睛区域或眼睑关键点EAR 低于阈值、PERCLOS 超过比例高,行业常以 PERCLOS 为金标准墨镜、强光、小目标漏检
哈欠嘴部区域或嘴部关键点MAR 持续多帧超过阈值中等,说话、吃东西干扰说话误判,麦克风噪声耦合
点头头部姿态角俯仰角连续低头中低,依赖相机标定看仪表盘、看后视镜会误报

闭眼通道是绝大多数商用 DMS 的主判据,原因很简单:人在真正入睡前,闭眼时长和闭眼频次的变化是最早出现的生理信号,而且和画面特征的耦合最直接。打哈欠和点头只能做辅助证据,不能单独触发告警,否则车内对话、低头看导航都会让系统疯狂误报。

把闭眼量化成模型指标,行业里有两套东西绕不过去。一套叫 PERCLOS,指单位时间内眼睑闭合时间所占的比例,这是被广泛引用的疲劳判据;另一套叫 EAR(Eye Aspect Ratio),用眼睛 6 个关键点的距离比来估计眼睛睁开程度。EAR 的最大好处是把“闭眼”从分类问题变成了回归问题,模型不用硬判断 open/closed,而是给一个连续值,阈值后期再说。

落地时 YOLO 在这里负责两件事:把人脸从整帧画面里框出来,再把眼睛区域从人脸框里找出来。人脸检测用 YOLO 非常合适,因为人脸尺度相对大、特征稳定;眼睛和嘴部是否闭合,则建议用关键点模型或者单独的眼睛分类模型去做,而不是让一个大 YOLO 把所有活都干了,原因在第 3 章展开。

2.2 用 YOLOv8 在本地跑通最小链路:人脸框、眼睛框与 EAR 计算

第一步先写一个通用的 EAR 计算函数,它接收 68 点人脸关键点坐标,返回左右眼的平均 EAR。这里用 dlib 的 68 点索引做示例:

import numpy as np # dlib 68 点中,左眼是 36~41,右眼是 42~47 LEFT_EYE = list(range(36, 42)) RIGHT_EYE = list(range(42, 48)) def eye_aspect_ratio(landmarks): # landmarks: 68x2 的坐标数组,来自人脸关键点模型 def ear(p): # 两条竖线距离的平均值除以水平线距离 a = np.linalg.norm(p[1] - p[5]) b = np.linalg.norm(p[2] - p[4]) c = np.linalg.norm(p[0] - p[3]) return (a + b) / (2.0 * c) return (ear(landmarks[LEFT_EYE]) + ear(landmarks[RIGHT_EYE])) / 2.0

这个函数每次只算一帧。关键点坐标必须与人脸框在同一坐标系里,否则 EAR 会被框的偏移直接带偏。常见做法是先拿 YOLO 人脸检测把人脸框出来,再在框内做关键点定位,整套链路如下:

from ultralytics import YOLO import cv2 face_model = YOLO("face_best.pt") # 只负责把人脸框出来 stream = cv2.VideoCapture(0) # 内视摄像头 while True: ok, frame = stream.read() if not ok: break results = face_model(frame, imgsz=640, conf=0.5) for box in results[0].boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 = box.astype(int) # 先对 face_roi 做 10% 扩边,防止人眼被裁掉一半 face_roi = frame[max(0, y1):y2, max(0, x1):x2] # 把 face_roi 送到 68 点关键点模型或你自己的 eye_cls 模型 # 这里以关键点模型为例,返回 68x2 坐标 pts = landmark_net(face_roi) ear = eye_aspect_ratio(pts) # ear 进入滑动窗口,后面的 PERCLOS 告警逻辑统一处理 push_ear(ear)

这段代码最容易被忽略的是扩边 10%:人脸检测框有时恰好贴着眉毛或眼眶边缘,直接把原框交给下级模型,眼睛的上下眼睑会被截掉,EAR 曲线会周期性出现尖峰,最终表现为不明原因误报。扩边不是调参玄学,是缺省步骤。

EAR 的取值范围在正常睁眼时大约 0.25 到 0.35,闭眼时掉到 0.1 以下。不要直接用 0.2 做全局阈值,不同人种、不同相机安装角度会让基线差 30% 以上。我一般会在系统启动后先采集 10 到 15 秒正常睁眼画面,把 EAR 的中位数作为个人基线,再按基线乘 0.6 作为闭眼阈值,这个逻辑留到第 6 章再给完整代码。

3. 模型选型与训练参数:单模型多类别为什么不如两级级联

疲劳检测项目里最常见的方案有两个方向:一个是用单个 YOLO 同时检测人脸、眼睛、嘴巴,输出多个类别;另一个是两级级联,先人脸后局部部件。二选一的时候,我强烈建议把预算留给级联方案。

3.1 一张图里人脸和眼睛尺度差 10 倍,单模型同时检测会互相拖累

先算一笔账。1080p 的内视摄像头画面里,驾驶员人脸高度通常在 200 到 400 像素,而眼睛高度只有 20 到 50 像素。YOLOv8 默认输入 640x640,经过 5 次下采样后特征图是 20x20,一个 30 像素高的眼睛在最后的特征图上只占不到 1 个像素。强行让一个模型的同一组 head 去预测 300 像素的人脸和 30 像素的眼睛,小目标类别在训练时会持续被损失函数里的大目标梯度淹没。

眼睛类别如果作为独立类别放进单模型,它还会被背景里的阴影、方向盘轮廓干扰,因为远距离下小目标的纹理信息本来就少。级联方案的思路是:第一个 YOLO 只做人脸,把人脸框裁出来缩放到 640 或 1280,眼睛区域在裁剪后的人脸图里占的比例直接放大好几倍,第二个模型再看这张大图时,眼睛就不再是“小目标”了。这也是为什么人脸模型可以用轻量 backbone,眼睛模型反而要保留足够分辨率的根本原因。

3.2 可抄的训练配置:face.yaml、eye.yaml 与这两条训练命令

先准备两套数据集配置。人脸模型只检测一个类,眼睛模型检测两个类:open 和 closed。如果你的标注里只有眼睛 bbox 没有状态,可以退一步用一个二分类模型处理眼睛区域,但效果通常比不上直接让 YOLO 学习“睁/闭”的空间特征。

# face.yaml path: /data/fatigue train: images/train val: images/val nc: 1 names: ["face"]
# eye.yaml path: /data/fatigue train: images/train val: images/val nc: 2 names: ["eye_open", "eye_closed"]

训练命令用 ultralytics 的标准入口,眼睛模型我建议把输入分辨率提到 1280:

# 人脸模型:从预训练权重继续训练,收敛快 yolo train data=face.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 patience=20 # 眼睛模型:目标是局部小区域,分辨率提到 1280,batch 相应减半 yolo train data=eye.yaml model=yolov8n.pt epochs=100 imgsz=1280 batch=8 patience=20

参数上有四个点值得说明。第一,两个模型都不要从随机权重开始,用 yolov8n.pt 做预训练,驾驶员疲劳数据集规模通常只有几千张,随机初始化会让训练过程极其漫长。第二,imgsz=1280 对显存要求高,如果显卡只有 8G,可以把 batch 降到 4,同时关闭 mosaic 增强,因为 mosaic 拼贴会让小尺寸的眼睛区域出现拼接伪影,反而损害精度。第三,patience=20 表示验证集连续 20 轮不提升就早停,这是防止过拟合最便宜的手段。第四,眼睛模型建议用 yolov8n 而不是 s/m/l,因为裁剪后人脸图已经很干净,模型容量不是瓶颈,推理速度才是。

训练完成后,推理时按第 2 章的链路把两个模型串起来:第一级只看整帧,第二级只看人脸框。这里有一个很多人不知道的细节:眼睛模型推理时,输入图像分辨率要和你训练时一致,如果训练用 1280,推理就别为了省时间直接塞 640,特征分布不一致会让置信度全线漂移。

4. 疲劳数据集构建:公开集、自采数据与 YOLOv8 训练前的一步转换

数据集是这种项目真正的护城河。模型掉点,八成是数据缺角;数据问题会在装车后集中爆发,而且排查成本极高。把公开集和自采数据统一成 YOLOv8 格式,是进入训练前必须做对的一步。

4.1 公开数据集能做什么,不能做什么:NTHU-DDD 与 YawDD 的边界

公开集里比较常用的是 NTHU-DDD(台湾清华大学驾驶模拟数据集),包含几十位受试者在驾驶模拟器里的视频,有正常、眨眼、打哈欠、困倦几类行为,场景覆盖白天和夜晚。它的价值在于行为类别定义得比较接近真实疲劳状态,适合拿来做预训练或者验证算法链路。YawDD 是另一个以打哈欠为主的驾驶室数据集,摄像头装在车内后视镜位置,视角和实车 DMS 摄像头接近,但要确认它和你目标车型的安装高度是否一致,视角差异会直接导致模型在实车上失效。

公开集的边界也很明显:模拟器环境背景单一,多为实验室灯光或普通室内光,缺少真实车队里的逆光、夜间红外补光、墨镜遮挡、颠簸运动模糊场景;受试者以亚洲面孔为主,欧美人种的眼部特征分布有明显差异。所以只靠公开集训练然后装车,几乎必然翻车。常见做法是公开集 + 自采小样本的组合:公开集做预训练,自采数据按目标车型、目标安装位、目标光线条件补充 500 到 2000 张关键帧。

4.2 把 VOC/自有标注转成 YOLOv8 格式:转换脚本与四个边界坑

很多公开集和标注工具导出的是 VOC 格式(XML),YOLOv8 需要的是每张图一个同名 txt,每行一个目标,格式是class_id cx cy w h,所有坐标都是归一化浮点数。下面这段脚本把 VOC 转成 YOLOv8 格式:

import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path, class_names): tree = ET.parse(xml_path) root = tree.getroot() w = int(root.find("size/width").text) h = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: continue # 只保留疲劳任务需要的类别 cls_id = class_names.index(name) box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) # YOLO 标签是归一化后的 cx, cy, w, h,全部除以图像宽高 cx = (x1 + x2) / 2.0 / w cy = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_name = os.path.basename(xml_path).replace(".xml", ".txt") with open(os.path.join(out_path, out_name), "w") as f: f.write("\n".join(lines))

这块代码逻辑本身不难,难在边界坑。第一个坑是坐标越界:标注软件偶尔会把框画出图像边界,x2 大于 w,y1 小于 0,直接转出来后 anchor 计算会出 NaN,训练直接中断。第二个坑是分母除零:某些异常图片 width 字段缺失或为 0,脚本要在转换前做校验。第三个坑是类别名大小写不一致,比如Head和head在同一个数据集里混着写,class_names.index 会抛异常。第四个坑是图片有 EXIF 旋转信息,手机或运动相机拍摄的图在读取时会被自动旋转,但 XML 里的坐标是按未旋转图像标注的,最终结果是标签全部偏移 90 度,这一步必须在预处理阶段统一处理。

数据划分同样有讲究。疲劳视频是连续帧,同一人在同一段路上的前后帧高度相似,如果按帧随机划分,训练集和验证集里会混入几乎相同的图像,验证指标虚高到 0.98 都不奇怪。实际部署后换成新司机立刻掉到 0.6。正确的做法是按视频片段或人员 ID 划分,保证同一个受试者的画面只出现在训练集或验证集之一。

4.3 增广按疲劳场景配:低光、墨镜与抖动模糊的参数怎么给

YOLOv8 自带的增广对通用目标检测有效,但对疲劳检测场景针对性不够。三个场景必须要手动补。第一是低光与夜间红外:常见做法是把图像转灰度,再叠加亮度扰动,gamma 取值在 0.5 到 1.2 之间随机采样,这样模型不会只依赖颜色信息判断眼睛闭合。第二是墨镜遮挡:用随机大小和位置的黑块覆盖眼睛区域,模拟太阳镜,否则戴墨镜时模型会输出高置信度的 closed,触发连续误报。第三是运动模糊:用高斯模糊核模拟车辆颠簸和抖动,核大小 3 到 7,角度随机,增强模型对模糊帧的鲁棒性。

这些增广不用全堆在训练时做,很多团队选择在数据准备阶段离线生成一部分,再混合进训练集,这样能直观看到各场景的样本占比,而不是在训练日志里黑盒调参。做疲劳检测数据集时,数量不是第一优先级,场景覆盖度才是。

5. 训练与部署最容易翻车的 5 个项目坑:从标签到推理延迟

这一章是踩坑记录,全部来自实际项目中反复出现过的问题。每条按现象、原因、解决三步写,照做能省掉至少一周的排查时间。

5.1 验证集精度高,装车后夜间场景却频繁误报

现象:模型在开源集和内部验证集上 mAP 都在 95 以上,装到车内夜间开灯后,告警每一两分钟触发一次。

原因:训练集里白天图像占 80% 以上,夜间图像占比太少,模型实际上学会了“低照度区域 = 闭眼”这个错误的捷径。验证集同样以白天为主,所以指标完全失真。

解决:按场景维度统计训练集分布,把白天比例控制在 60% 左右,夜间和黄昏补足到 30% 到 40%。补不到实拍数据时,用第 4.3 节的灰度增强和亮度扰动强行制造夜间样本,但要注意灰度图只是模拟,和真实红外补光下的成像质感仍有差距,不能完全替代夜拍。

5.2 戴墨镜时眼睛误检,疲劳告警连续触发

现象:测试人员戴墨镜进入驾驶位,系统立刻报警,摘掉墨镜后恢复正常。

原因:墨镜区域在图像里是一块近乎纯黑的平面,没有纹理也没有眼睑轮廓,模型把它识别成闭眼是必然结果。这是数据缺陷,不是模型缺陷。

解决:最稳的方案是增加一个墨镜检测状态,先判断有没有戴墨镜,如果戴了就不启用眼睛通道,改用头部姿态和驾驶行为作为辅助判据。另一个折中方案是训练时做墨镜遮挡增广,让模型对“纯黑区域”不再直接对应到闭合状态,但这会牺牲一部分真实闭眼的召回,因为真实闭眼和墨镜在视觉特征上确实接近,只能靠上下文区分。

5.3 眼睛目标太小,两级级联后仍漏检

现象:人脸框裁切后,眼睛模型在距离摄像头稍远的坐姿下频繁漏检,尤其是倒装眼镜框或眯眼状态。

原因:人脸模型框住人脸后,裁切区域的分辨率取决于人脸在原始图像中的高度。驾驶员座椅靠后时,人脸高度可能只有 120 像素,眼睛区域只剩十几个像素,再送到眼睛模型里信息量不够。

解决:三步走。第一,人脸推理时把输入分辨率从 640 提到 960,人脸框尺寸会明显变大。第二,眼睛模型训练和图 3.2 一致用 1280,并且裁切后做人脸对齐,把眼睛固定到图像中相对稳定的位置。第三,如果还不行,用 SAHI 切片推理的思路对眼睛区域做局部放大,但推理成本会上升,通常只在高端算力平台上使用。

5.4 随机划分数据集导致同人同路段泄底,指标虚高

现象:训练过程正常,验证集 mAP 高达 98,但模型在另一批陌生人身上效果极差,回调一查,发现验证集里混着训练视频的同帧或相邻帧。

原因:数据划分用了全局随机采样,没有考虑视频的时间连续性和人员身份。连续帧之间的差异极小,模型在训练时已经见过几乎一样的画面,验证时只是“背答案”。

解决:划分单位从“帧”改成“视频片段”或“受试者 ID”。具体来说,把同一个视频文件的所有帧,或者同一个人在不同时间段的所有画面,全部划到同一个集合,绝不允许跨集合。这是疲劳检测数据管理里最容易忽视、又最影响指标可信度的一步。

5.5 两个模型串行推理,30fps 摄像头只跑到 8fps

现象:人脸模型跑 640 分辨率约 11ms,眼睛模型跑 1280 分辨率约 18ms,两模型串行后整体只有不到 9fps,车规级平台更慢。

原因:人脸检测每帧都跑,眼睛模型每帧都跑,且眼睛模型的 1280 分辨率推理在高性价比边缘盒子上非常吃力。

解决:不要每帧都做完整推理。第一,眼睛模型按 1/3 帧率运行,即每 3 帧处理一次,帧间用上一状态保持,因为人眼闭合是一个持续过程,200ms 间隔足够。第二,人脸模型换更小输入 480x480,对人脸这种大目标影响极小。第三,最后再做 int8 量化和 TensorRT 转换,ultralytics 的 export 命令直接支持,精度损失在疲劳检测这种二分类任务里通常可以接受。

6. 时序判定与阈值校准:从单帧闭眼到 PERCLOS 告警的一次落地

疲劳告警不能看单帧。人正常眨眼时闭眼只有 100 到 150 毫秒,单帧误判会导致系统频繁打扰司机。把第 2 章算出的 EAR 送进滑动窗口,做 PERCLOS 统计,才能得到稳定可靠的告警信号。

from collections import deque ear_buf = deque(maxlen=150) # 5 秒 x 30fps WARN_PERCLOS = 0.4 # 闭眼帧占比超过 40% while True: ear = next_ear_value() # 来自第 2 章或第 3 章模型的输出 ear_buf.append(ear) if len(ear_buf) < ear_buf.maxlen: continue perclos = sum(1 for v in ear_buf if v < EAR_CLOSED) / len(ear_buf) if perclos >= WARN_PERCLOS: issue_warning()

PERCLOS 的判定阈值建议在 0.35 到 0.4 之间,过低容易把连续正常眨眼误报成疲劳,过高会漏掉真正的困倦状态。更合理的做法是分两级告警:PERCLOS 超过 0.35 持续 3 秒触发提示音;超过 0.5 并持续 8 秒触发强烈告警。时长阈值比 PERCLOS 阈值更影响实际体验,耐心观察真实数据再定。

阈值校准是最后一道关键工序。不同人的 EAR 基线差异明显,大眼睛和小眼睛在完全睁眼时的 EAR 可以差 0.1 以上,固定阈值必然误报或漏报。我一般在系统启动后采集 30 秒正常状态数据,取 EAR 中位数作为个人基线,再乘 0.65 作为闭眼阈值,效果比任何全局固定值都稳。这个校准逻辑也要考虑座椅位置和后视镜遮挡,所以每次启动都重新校准一次。

验证阶段建议录一段 15 分钟模拟驾驶视频,找人逐帧标注闭眼区间,再对比算法输出,计算帧级 F1 而不是只看告警次数。第一次做的时候,我在验证集上把 PERCLOS 阈值调到 0.3,指标很好,但装到车上全在误报,最后发现是测试时摄像头安装角度比训练数据低,人脸框裁切位置整体偏下,EAR 曲线整体被抬高。后来我在代码里加了 30 秒自适应校准,让不同座椅高度的人上车后都能自动纠正阈值,问题才真正解决。这种问题不会在模型训练阶段暴露,只能靠现场数据和耐心试。给自己留足现场调试的时间,比调任何网络结构都值得。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:46:50

Jev模型实战:LangChain与LangGraph中的结构化输出与自动化测试脚本生成

1. 从“不说话的模型”说起&#xff1a;Jev 到底是个什么东西 第一次看到“Jev&#xff1a;一个不说话的模型”这个标题&#xff0c;我脑子里蹦出来的第一个念头是——这年头还有模型不爱说话&#xff1f;毕竟从 ChatGPT 开始&#xff0c;大家已经习惯了模型“话痨”式的交互方…

作者头像 李华
网站建设 2026/10/1 12:46:32

OpenRig 实战指南:Node.js + tmux + YAML + Codex 四件套本地AI开发基座搭建

1. OpenRig 是什么&#xff1a;一个被严重误读的开源项目名称OpenRig 这个词在当前中文技术社区里&#xff0c;正经历一场典型的“语义漂移”——它既不是某个广为人知的成熟开源框架&#xff0c;也不是某家大厂发布的官方工具套件&#xff0c;更不是 Codex、Node.js 或 YAML 的…

作者头像 李华
网站建设 2026/10/1 12:45:33

无独显16G内存本地部署大模型:Ollama与llama.cpp实战指南

1. 低配电脑跑大模型&#xff0c;先搞清楚你到底在折腾什么先把结论摆在前面&#xff1a;2026 年了&#xff0c;一台没有独立显卡、只有 16G 内存的普通办公电脑&#xff0c;本地部署 AI 大模型这件事&#xff0c;能跑&#xff0c;但能跑的东西和你想象中的东西&#xff0c;大概…

作者头像 李华
网站建设 2026/10/1 12:45:29

DeepSeek Harness:面向开发者的轻量级工程化封装实践

1. 项目概述&#xff1a;DeepSeek Harness 不是“插件商店”&#xff0c;而是开发者手里的工程化杠杆最近在多个技术社区和开发群聊里&#xff0c;频繁看到“DeepSeek Harness 插件推荐”这类搜索词——但说实话&#xff0c;第一次看到时我愣了一下&#xff1a;DeepSeek 官方压…

作者头像 李华
网站建设 2026/10/1 12:44:54

SpringBoot+Vue+MVC架构:文物征集管理系统从设计到部署全解析

做管理系统这么多年&#xff0c;SpringBoot Vue 这套组合我搭过不少&#xff0c;但真正把 MVC 模式从头到尾理得特别清楚&#xff0c;是在做这套文物征集管理系统之后。技术栈没什么花哨的&#xff0c;就是 SpringBoot、Vue、MVC 模式、MyBatis 和 MySQL&#xff0c;从文物预征…

作者头像 李华