ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用YOLOv8和OpenCV实现双马尾检测:目标检测与图像处理的趣味AI实践

2026/8/31 3:19:11 拓冰建站 浏览量
用YOLOv8和OpenCV实现双马尾检测:目标检测与图像处理的趣味AI实践 警告检测到广东双马尾出没这个标题第一眼看上去像是网络热梗实际上它也确实是。在短视频和直播生态里“双马尾”逐渐变成了一种辨识度很高的视觉符号而“广东双马尾”更多是网友玩出来的一个梗不同地区、不同人群之间并没有任何可比性。这里我们不讨论梗的出处也不做地域话题而是把它当成一个轻松的项目代号用 YOLOv8 检测画面中的人物再用 OpenCV 分析人物头部区域的对称特征最终在画面上输出一句带警告感的提示——“检测到双马尾出没”。整个项目可以跑在笔记本上包含图片检测、摄像头实时检测、Web 接口部署三个部分。无论你是刚入门目标检测还是想练习 OpenCV 图像处理又或者是想找一个能快速演示给朋友看的 AI 小工具这个项目都挺合适。1. 项目背景与检测原理1.1 项目定位先明确一下这不是一个工业级发型识别系统而是一个“能跑、能演示、能学习”的趣味 AI Demo。它的核心逻辑可以拆成两段先用目标检测模型把画面中的人找出来。再对人物头部区域做图像分析判断是否具有“双马尾”的视觉特征。为什么这样拆因为“双马尾”并不是一个严格的物体类别。它不是猫、不是车、不是行人很难用一个语义清晰的边界框来定义。双马尾本质上是发型特征两条辫子或两束头发从头部两侧对称垂下。这种特征如果直接训练一个目标检测模型最大的问题是数据集非常难搞需要大量人工标注而且在不同角度、不同遮挡、不同发色下标注标准很难统一。所以更务实的方案是用人检测模型确定人的位置再裁剪出头部区域在头部区域上用图像处理的方式检测“左右两侧是否存在对称的头发束”。这个方案足够简单也能达到演示效果。1.2 整体技术方案这个项目用到的技术栈如下模块作用选型目标检测检测画面中的人物位置YOLOv8图像处理裁剪头部区域、分析头发像素分布OpenCV运行环境Python 虚拟环境conda / venvWeb 接口提供 HTTP 检测接口Flask快速演示网页上传图片检测GradioYOLOv8 是 Ultralytics 开源的目标检测框架目前入门门槛很低官方预训练权重可以直接使用检测效果也稳定。OpenCV 则是图像处理领域的老牌工具用来做颜色空间转换、像素遮罩、区域裁剪都非常方便。整个检测流程可以用下面这条链路概括输入图像 → YOLOv8 检测人物 → 提取头部区域 → 头发像素遮罩 → 左右对称性判断 → 输出结果1.3 为什么不用单一深度学习模型可能有人会问为什么不直接用深度模型分类非要用手工图像特征这里要说明一下。如果用分类模型比如 ResNet、Vision Transformer我们需要一个“双马尾数据集”。这类数据集的构建成本很高而且双马尾的视觉变体太多黑发双马尾、金发双马尾、麻花辫、双丸子头、短发扎起来的小揪揪……分类模型很容易过拟合到颜色和背景上导致换个环境就失效。用目标检测模型也有类似问题。双马尾不是一个有明确轮廓的物体边界框很难画。如果是两个辫子各画一个框那很多披肩发也会被误判。如果对整个人画框那模型学习到的更多是人形特征而不是发型特征。所以这个项目采用“检测 规则”的混合方案一方面降低了对数据集的要求另一方面也让代码可视化程度更高。你可以直接用调试模式查看头部区域、头发遮罩、左右分割的结果每一步都看得见摸得着学习价值比黑盒模型更高。2. 环境准备与依赖安装2.1 环境说明本文以 Python 3.9 环境为例操作系统为 WindowsmacOS 和 Linux 的命令几乎一致。因为 YOLOv8 依赖 PyTorch如果你的电脑有 NVIDIA 显卡并且安装了 CUDA推理速度会快很多没有显卡也没关系CPU 推理也能运行只是速度稍慢。版本方面不建议完全照搬建议按实际环境调整。核心依赖如下依赖包主要用途ultralyticsYOLOv8 目标检测opencv-python图像处理与摄像头读取flaskWeb 接口gradio快速页面演示2.2 创建虚拟环境推荐用 conda 创建独立虚拟环境避免污染系统 Python。conda create -n twin-tail python3.9 -y conda activate twin-tail如果你没有安装 conda也可以用 venvpython -m venv twin-tail-env # Windows twin-tail-env\Scripts\activate # macOS / Linux source twin-tail-env/bin/activate2.3 安装依赖激活环境后执行下面的命令安装依赖pip install ultralytics opencv-python flask gradio安装完成后建议先验证一下环境是否正常import cv2 from ultralytics import YOLO print(OpenCV version:, cv2.__version__) model YOLO(yolov8n.pt) print(YOLO model loaded:, model)第一次加载yolov8n.pt时Ultralytics 会自动从官方仓库下载权重。如果网络较慢可以手动下载后放到当前目录。权重文件不大大概 6MB 左右。3. 核心流程拆解3.1 YOLOv8 检测人物YOLOv8 的用法非常简洁一行代码就能完成检测from ultralytics import YOLO model YOLO(yolov8n.pt) results model(test.jpg, classes[0], conf0.4)这里的classes[0]表示只检测类别 0即“person”人类别。COCO 数据集共有 80 个类别person 是索引 0。conf0.4表示置信度阈值低于 0.4 的检测框会被过滤掉。results是一个包含检测结果的列表每个元素对应一张输入图片。我们可以从结果中获取边界框坐标for result in results: boxes result.boxes.xyxy.cpu().numpy() for box in boxes: x1, y1, x2, y2 map(int, box) print(x1, y1, x2, y2)xyxy表示边界框的左上角坐标和右下角坐标。拿到坐标后就可以在原图上裁剪人物区域。3.2 头部区域定位这一步是算法的关键。假设 YOLO 给出的边框覆盖整个人体那么头部通常位于边框靠上方的区域。一般来说人物检测框的顶部约 0 到 5% 是头部上方背景5% 到 25% 左右是面部和头发区域。为了稳健一点我建议取顶部 2% 到 30% 的区域作为“头部候选区”。代码实现如下def extract_head_region(image, box): x1, y1, x2, y2 map(int, box) height y2 - y1 head_top y1 int(height * 0.02) head_bottom y1 int(height * 0.30) # 防止越界 head_top max(0, head_top) head_bottom min(image.shape[0], head_bottom) if head_bottom head_top: return None head_roi image[head_top:head_bottom, x1:x2] return head_roi为什么取 30% 而不是 20%因为双马尾的视觉特征有一部分在头部两侧偏下的位置如果只取很小的区域容易漏掉左右两侧的头发束。取 30% 左右可以兼顾面部和上半部分头发。3.3 头发遮罩与对称性判断有了头部区域之后下一步就是判断“左右两侧是否有头发”。头发像素的特征比较明显通常比肤色暗颜色偏黑、偏棕、偏黄。我们可以结合肤色检测和亮度检测来提取头发遮罩。先看肤色检测。OpenCV 自带 YCrCb 颜色空间肤色在 YCrCb 空间里有一个比较经典的取值范围ycrcb cv2.cvtColor(head_roi, cv2.COLOR_BGR2YCrCb) skin cv2.inRange(ycrcb, (0, 133, 77), (255, 173, 127))这里skin是一个二值遮罩白色区域表示肤色黑色区域表示非肤色。头发通常是深色像素我们可以把灰度值低于阈值的像素认为是“深色像素”gray cv2.cvtColor(head_roi, cv2.COLOR_BGR2GRAY) dark gray 100最后把“深色像素”和“非肤色像素”取交集hair dark (skin 0)hair就是一个布尔矩阵值为 True 表示该像素是头发候选像素。接下来把头部区域分成左右两半分别统计头发像素比例w head_roi.shape[1] half w // 2 left_region hair[:, :half] right_region hair[:, half:] left_ratio left_region.sum() / max(1, left_region.size) right_ratio right_region.sum() / max(1, right_region.size)双马尾的特征是左右两侧都有一定量的头发而且比较对称。基于这个特点可以设计两个判断条件左侧头发比例大于阈值。右侧头发比例大于阈值。左右两侧头发比例的差值不太大。代码实现def is_twin_tail(left_ratio, right_ratio, thresh0.15, sym_thresh0.5): if left_ratio thresh or right_ratio thresh: return False diff abs(left_ratio - right_ratio) / max(left_ratio, right_ratio) return diff sym_thresh3.4 完整检测函数把上面的逻辑组合起来写成一个独立的检测函数def detect_twin_tail(image, box): head_roi extract_head_region(image, box) if head_roi is None: return False, None ycrcb cv2.cvtColor(head_roi, cv2.COLOR_BGR2YCrCb) skin cv2.inRange(ycrcb, (0, 133, 77), (255, 173, 127)) gray cv2.cvtColor(head_roi, cv2.COLOR_BGR2GRAY) dark gray 100 hair dark (skin 0) w head_roi.shape[1] if w 2: return False, None half w // 2 left_region hair[:, :half] right_region hair[:, half:] left_ratio left_region.sum() / max(1, left_region.size) right_ratio right_region.sum() / max(1, right_region.size) if left_ratio 0.15 or right_ratio 0.15: return False, {left: left_ratio, right: right_ratio} diff abs(left_ratio - right_ratio) / max(left_ratio, right_ratio) is_twin diff 0.5 return is_twin, {left: left_ratio, right: right_ratio, diff: diff}这里需要注意这个算法是启发式规则不是训练出来的模型。遇到金发、染发、高光等情况时gray 100可能判断不准。在后续小节中我们会看到如何调试和调参。4. 实战图片检测4.1 项目目录结构为了便于管理推荐把项目文件组织成下面的结构twin-tail-project/ ├── main.py ├── detector.py ├── test.jpg └── requirements.txt其中detector.py存放检测逻辑main.py作为入口脚本test.jpg是测试图片。4.2 编写检测器模块先编写detector.py把检测逻辑封装成可复用函数import cv2 import numpy as np from ultralytics import YOLO def load_model(model_pathyolov8n.pt): return YOLO(model_path) def extract_head_region(image, box): x1, y1, x2, y2 map(int, box) height y2 - y1 head_top y1 int(height * 0.02) head_bottom y1 int(height * 0.30) head_top max(0, head_top) head_bottom min(image.shape[0], head_bottom) if head_bottom head_top: return None return image[head_top:head_bottom, x1:x2] def analyze_head_roi(head_roi): ycrcb cv2.cvtColor(head_roi, cv2.COLOR_BGR2YCrCb) skin cv2.inRange(ycrcb, (0, 133, 77), (255, 173, 127)) gray cv2.cvtColor(head_roi, cv2.COLOR_BGR2GRAY) dark gray 100 hair dark (skin 0) w head_roi.shape[1] if w 2: return False, None half w // 2 left_region hair[:, :half] right_region hair[:, half:] left_ratio left_region.sum() / max(1, left_region.size) right_ratio right_region.sum() / max(1, right_region.size) if left_ratio 0.15 or right_ratio 0.15: return False, { left: round(left_ratio, 3), right: round(right_ratio, 3) } diff abs(left_ratio - right_ratio) / max(left_ratio, right_ratio) is_twin diff 0.5 return is_twin, { left: round(left_ratio, 3), right: round(right_ratio, 3), diff: round(diff, 3) } def detect_persons(model, image, conf0.4): results model(image, classes[0], confconf) boxes [] for result in results: if result.boxes is None: continue tmp result.boxes.xyxy.cpu().numpy() boxes.extend(tmp.tolist()) return boxes def draw_warning(image, box): x1, y1, x2, y2 map(int, box) cv2.rectangle(image, (x1, y1), (x2, y2), (0, 0, 255), 2) label Warning! Twin Tail Detected! cv2.putText( image, label, (x1, max(30, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2 ) return image4.3 编写主入口脚本再编写main.pyimport cv2 from detector import load_model, detect_persons, analyze_head_roi, extract_head_region model load_model(yolov8n.pt) image cv2.imread(test.jpg) boxes detect_persons(model, image) for box in boxes: head_roi extract_head_region(image, box) if head_roi is None: continue is_twin, metrics analyze_head_roi(head_roi) print(检测框:, box, 指标:, metrics) if is_twin: image draw_warning(image, box) cv2.imwrite(output.jpg, image) print(检测完成结果已保存到 output.jpg)4.4 运行与验证在项目目录下执行python main.py如果一切正常终端会输出类似下面的信息检测框: [112.0, 45.0, 398.0, 540.0] 指标: {left: 0.32, right: 0.27, diff: 0.156} 检测完成结果已保存到 output.jpgleft和right分别表示左右半区头发像素占比diff表示左右不对称程度。如果diff小于 0.5并且两侧占比都大于 0.15就会判定为“检测到双马尾”。这里要特别说明如果测试图片的背景比较复杂比如深色背景、深色衣服、帽子遮挡等检测效果可能会有偏差。这很正常因为规则本身的鲁棒性有限。本节的重点是理解整个检测链路是如何工作的。5. 实战摄像头实时检测图片检测跑通之后接下来把它升级成摄像头实时检测。这样打开摄像头如果画面中出现“双马尾”屏幕上就会直接弹出红色警告框。实时检测的代码逻辑与图片检测基本一致区别在于需要循环读取摄像头帧并对每一帧执行检测。5.1 编写实时检测脚本下面创建一个新文件camera.pyimport cv2 from detector import load_model, detect_persons, analyze_head_roi, extract_head_region model load_model(yolov8n.pt) cap cv2.VideoCapture(0) if not cap.isOpened(): print(无法打开摄像头请检查摄像头权限或编号) exit(1) while True: ret, frame cap.read() if not ret: print(读取视频帧失败) break boxes detect_persons(model, frame, conf0.45) for box in boxes: head_roi extract_head_region(frame, box) if head_roi is None: continue is_twin, metrics analyze_head_roi(head_roi) if is_twin: x1, y1, x2, y2 map(int, box) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) label Warning! Twin Tail Detected! cv2.putText( frame, label, (x1, max(30, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2 ) cv2.imshow(Twin Tail Detector, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()运行方式python camera.py按q键退出程序。5.2 实时检测的注意点实时检测时有几个细节需要留意第一摄像头读取默认使用设备 0。如果你的电脑有多个摄像头可以改成cv2.VideoCapture(1)试一下。第二实时视频帧的分辨率通常比较高而 YOLOv8n 是轻量模型CPU 推理速度正常在 2 到 5 帧每秒左右。如果觉得卡顿可以先用cv2.resize把每一帧缩小frame_small cv2.resize(frame, (640, 640))检测完成后再把边界框映射回原图尺寸。第三因为实时视频包含连续帧某些帧可能突然误检另一些帧漏检。如果想更稳定可以加入一个简单的“连续 N 帧都检测到才触发”的逻辑减少闪烁。6. 通过 Web 接口提供服务如果不想在命令行跑也可以直接把检测能力封装成 HTTP 接口。这样其他程序、手机端都可以调用。6.1 Flask API新建app.py实现一个简单的图片上传接口import cv2 import numpy as np from flask import Flask, request, jsonify from detector import load_model, detect_persons, analyze_head_roi, extract_head_region app Flask(__name__) model load_model(yolov8n.pt) app.route(/detect, methods[POST]) def detect(): if image not in request.files: return jsonify({error: no image uploaded}), 400 file request.files[image].read() img_array np.frombuffer(file, np.uint8) image cv2.imdecode(img_array, cv2.IMREAD_COLOR) if image is None: return jsonify({error: image decode failed}), 400 boxes detect_persons(model, image) result [] for box in boxes: head_roi extract_head_region(image, box) if head_roi is None: continue is_twin, metrics analyze_head_roi(head_roi) if is_twin: x1, y1, x2, y2 map(int, box) result.append({ box: [x1, y1, x2, y2], metrics: metrics }) return jsonify({ twin_tail_count: len(result), boxes: result }) if __name__ __main__: app.run(host0.0.0.0, port5000)启动服务python app.py6.2 使用 curl 测试打开另一个终端用 curl 发送测试图片curl -X POST -F imagetest.jpg http://127.0.0.1:5000/detect返回结果示例{ twin_tail_count: 1, boxes: [ { box: [112, 45, 398, 540], metrics: { left: 0.32, right: 0.27, diff: 0.156 } } ] }twin_tail_count为 1表示画面中检测到 1 个符合双马尾特征的人物。6.3 用 Gradio 快速做可视化页面如果觉得 curl 不够直观可以用 Gradio 在浏览器里上传图片并查看结果。新建gradio_app.pyimport cv2 import numpy as np import gradio as gr from detector import load_model, detect_persons, analyze_head_roi, extract_head_region model load_model(yolov8n.pt) def infer(image): boxes detect_persons(model, image) for box in boxes: head_roi extract_head_region(image, box) if head_roi is None: continue is_twin, metrics analyze_head_roi(head_roi) if is_twin: x1, y1, x2, y2 map(int, box) cv2.rectangle(image, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText( image, Warning! Twin Tail Detected!, (x1, max(30, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2 ) return image gr.Interface( fninfer, inputsgr.Image(typenumpy), outputsgr.Image(typenumpy), titleTwin Tail Detector ).launch(shareFalse)运行python gradio_app.py浏览器会自动打开一个页面上传图片后即可看到检测结果。shareFalse表示只在本地访问如果希望公网临时访问可以改成shareTrue但要注意隐私问题。7. 常见问题与排查思路这个项目在实际运行时会遇到一些常见问题。下面整理成表格方便快速对照排查。问题现象常见原因解决思路打开摄像头时报错无法打开摄像头摄像头编号不对或权限未开启改成cv2.VideoCapture(1)重试检查系统是否允许应用访问摄像头下载yolov8n.pt速度很慢网络访问模型仓库不稳定手动下载权重文件放到项目目录或用镜像源加速检测不到任何人置信度阈值太高或图片中人太小将conf从 0.4 降到 0.25适当提高图片分辨率画面中的人被误判为双马尾头发遮罩把深色衣服也算进去了调整gray 100的阈值或缩小头部区域范围双马尾实际存在但没检出来头发颜色偏浅或左右两侧被遮挡使用灰度直方图自适应阈值而不是固定 100实时检测特别卡顿视频帧分辨率太高CPU 推理慢将帧缩小到 640 或 512 再检测检测框再映射回原图Flask 接口返回图片解码失败上传的不是真实图片或请求字段名不对确认使用image字段上传且图片格式为 jpg/png女生扎的是普通马尾也会被误判普通单马尾只有一侧头发量明显提高左右对称性阈值例如要求diff 0.3这里最需要说明的是本项目的识别核心是对称性判断。单马尾通常只有一侧有大量头发左右比例差距很大所以不会被判定为双马尾。但如果背景复杂、拍摄角度倾斜对称性判断会受到很大影响。8. 工程建议与隐私边界8.1 数据合规与隐私保护这个项目涉及摄像头实时拍摄和图片上传必须注意隐私问题。在实际使用中建议做到以下几点摄像头画面尽量在本地处理不要自动上传到服务器。如果使用 Flask 或 Gradio 部署成服务建议在内网或本机使用不暴露到公网。不存储用户上传的图片处理完立即释放内存。不对画面中的个体进行身份识别。双马尾检测只做发型视觉特征分析不应该关联到任何个人身份信息。8.2 不要做成“人脸识别”工具这个项目的检测对象是人物本身而不是人的身份。在代码中我们只使用了边界框、头部区域和头发像素统计没有调用任何人脸识别 API也没有提取人脸特征向量。这一点非常关键。如果以后想把这个项目扩展成真实产品一定要先明确用途。涉及人脸、人体特征分析的项目在数据采集、模型训练、上线部署环节都可能涉及合规问题需要提前咨询法律和技术负责人不能想当然地直接上线。8.3 防止地域刻板印象“广东双马尾”在网络上是一个梗但这个梗不应该被解读成对某个地区人群的刻板标签。双马尾只是一种发型和地域没有任何关系。在写项目文档、演示代码、对外宣传时建议弱化地域标签突出技术本身。否则不但容易引发误解还会让项目偏离技术学习的初衷。8.4 参数调优与生产化思路如果希望把检测效果做得更好可以从两个方向改进。第一个方向是优化头发遮罩。目前固定使用gray 100在暗光环境下会把背景误判为头发。可以使用大津法Otsu自动计算分割阈值_, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) dark binary 255第二个方向是引入关键点检测。MediaPipe 提供了人脸网格和人体姿态关键点可以更精确地定位头部中心、左右脸颊位置从而把头部区域划分得更准确。双马尾的视觉特征本质上是“头部两侧有大块对称的头发区域”利用关键点可以显著减少背景干扰。如果追求真正的生产级效果还是需要自建数据集并训练分类或分割模型。比如采集一批人物图片标注“无马尾”“单马尾”“双马尾”“扎发”等类别再用 YOLOv8 的分类模式进行训练。但这个时间成本高适合作为进阶学习路线。9. 总结与扩展方向到这里一个完整的“警告检测到双马尾出没”趣味 Demo 就搭建完成了。我们做了四件事使用 YOLOv8 完成人物检测。使用 OpenCV 完成头部区域裁剪、肤色检测、头发遮罩提取。通过左右头发比例与对称性判断双马尾特征。将检测能力扩展到摄像头实时检测、Flask 接口和 Gradio 页面。整个项目麻雀虽小五脏俱全覆盖了目标检测、图像处理、接口封装、前端演示的完整链路。你可以在此基础上继续扩展比如把检测对象换成帽子、口罩、宠物猫狗只要修改对应的图像特征规则即可。也可以把规则判断替换成训练好的深度分类模型让准确率更高。最后提醒一句这个识别规则是启发式的不是深度学习模型训练出来的精度有限。真正做发型识别需要自建数据集并训练分类模型。不过当你把 YOLOv8、OpenCV、Flask 这几个工具串起来以后再做类似的小工具就会顺畅很多。赶紧动手跑一遍代码也许你也能做出一个属于自己的“XX 出没检测器”。