在很多计算机视觉项目中,目标检测或分割的结果往往只用于“显示框”或“统计数据”。但如果我们换一个思路:
👉 把视觉模型的输出当作“交互边界”,是不是就能做出一些更有趣的实时效果?
这篇文章分享一个基于 YOLO 实例分割(Segmentation Mask) 的小实验:
画面中会漂浮一些泡泡,当泡泡碰到被分割出来的目标区域时,就会“爆炸”。


一、为什么选择 YOLO + Segmentation?
1. YOLO 不只是检测框
大多数人对 YOLO 的印象是这样的:
-
输入一帧图像
-
输出若干个
bbox + class + confidence
但从 YOLOv8 开始,YOLO 在实时性不变的前提下,已经原生支持了:
-
实例分割(Instance Segmentation)
-
每个目标都附带一个 像素级掩码(Mask)
这让 YOLO 非常适合做 实时交互型视觉应用,例如:
-
人体轮廓交互
-
AR / 特效遮挡
-
基于区域的物理反馈
2. Segmentation Mask 的价值
相比 bounding box,分割掩码有三个关键优势:
-
像素级精度
不是“框住一个人”,而是精确到轮廓 -
天然的交互边界
掩码本身就是一个二维碰撞区域 -
无需额外后处理
不需要复杂的几何计算,直接查像素即可
在本文的代码中,掩码被直接转成了一个 uint8 的二维数组:
mask = result.masks.data[0].cpu().numpy() mask = (mask * 255).astype(np.uint8)
这一步非常关键 —— 它让深度学习的输出,变成了“可被游戏逻辑直接使用的数据结构”。
二、整体设计思路
系统结构拆解
整个交互效果可以拆分为四个部分:
摄像头 / 视频帧 ↓ YOLO 实例分割 ↓ Seg Mask(碰撞区域) ↓ 泡泡物理系统 + 渲染
YOLO 只负责一件事:
👉 告诉我们哪些像素属于“可交互目标”
其余逻辑全部是传统 CV + 交互动画。
三、泡泡系统设计
1. 泡泡的数据结构
每一个泡泡都是一个轻量级的物理对象:
{ "x": float, "y": float, "vx": float, "vy": float, "alive": bool, "boom": int, "phase": float }
设计要点:
-
vx / vy:控制随机漂浮
-
phase:用于呼吸动画的相位差
-
boom:爆炸残留帧数(视觉闪光)
这让每个泡泡在视觉上都显得「不那么机械」。
2. 泡泡生成策略
泡泡不会生成在分割区域内部:
if mask[y, x] == 0: # 才允许生成
这看似简单,但效果上非常重要:
-
泡泡只存在于「背景空间」
-
人或物体一进入画面,就天然形成“不可生成区”
这本身就是一种无接触式交互设计。
四、用 Mask 做“碰撞检测”
最核心的一行逻辑
if mask[y, x] > 0: b["alive"] = False b["boom"] = 6
没有复杂的几何算法,也没有多边形计算:
-
泡泡的位置 →
(x, y) -
掩码像素值 → 是否大于 0
-
命中即触发事件
这正是 Segmentation 在交互场景中的最大优势。
为什么这种方式非常高效?
-
O(1) 时间复杂度
-
完全在 CPU 上就能跑
-
非常适合实时视频流
这也是为什么这种思路非常适合:
-
WebCam 实时交互
-
展示屏装置
-
轻量 AR 应用
五、视觉表现:让泡泡“有生命”
代码中对视觉细节做了几件很关键的事情。
1. 呼吸式半径变化
pulse = 1.0 + 0.15 * sin(frame + phase)
结果是:
-
泡泡不会静止
-
画面始终有微弱节奏感
2. 半透明叠加
cv2.addWeighted(overlay, 0.25, img, 0.75, 0, img)
这让泡泡看起来更像:
-
水
-
果冻
-
光学体
而不是一个“画上去的圆”。
3. 爆炸残影
即使泡泡“死亡”,也不会立刻消失,而是:
-
留下 6 帧的扩散圆
-
给用户一个清晰的“反馈确认”
这是非常典型的交互反馈设计原则。
六、这类设计能用在哪里?
这种 “CV + 轻物理 + 特效反馈” 的模式,其实有非常多应用场景:
-
商场互动屏
-
展览装置
-
儿童互动教育
-
摄像头驱动的网页特效
-
游戏原型验证
你甚至可以把:
-
泡泡 → 粒子
-
爆炸 → 音效 / 分数
-
人体 → 操作输入


import cv2
import numpy as np
import random
import math
#===========# 免责声明 #===========#
# 您已进入开发者模式,平台视为您已具备 Python 编程能力。
#==================================#
class BubblePlay:
def __init__(self):
# 泡泡配置
self.bubble_count = 12
self.bubble_radius = 48
self.max_speed = 3
self.bubbles = []
self.initialized = False
self.frame_idx = 0
# =========================
# 泡泡生成
# =========================
def _generate_bubbles(self, mask, h, w):
self.bubbles = []
for _ in range(self.bubble_count):
for _ in range(100):
x = random.randint(self.bubble_radius, w - self.bubble_radius)
y = random.randint(self.bubble_radius, h - self.bubble_radius)
if mask[y, x] == 0:
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(1, self.max_speed)
self.bubbles.append({
"x": float(x),
"y": float(y),
"vx": math.cos(angle) * speed,
"vy": math.sin(angle) * speed,
"alive": True,
"boom": 0, # 爆炸闪光帧
"phase": random.uniform(0, 2 * math.pi)
})
break
# =========================
# 更新泡泡状态
# =========================
def _update_bubbles(self, mask, h, w):
alive_count = 0
for b in self.bubbles:
if not b["alive"]:
if b["boom"] > 0:
b["boom"] -= 1
continue
b["x"] += b["vx"]
b["y"] += b["vy"]
x, y = int(b["x"]), int(b["y"])
# 边界反弹
if x <= self.bubble_radius or x >= w - self.bubble_radius:
b["vx"] *= -1
if y <= self.bubble_radius or y >= h - self.bubble_radius:
b["vy"] *= -1
# 碰 mask → 爆炸
if 0 <= x < w and 0 <= y < h and mask[y, x] > 0:
b["alive"] = False
b["boom"] = 6
else:
alive_count += 1
return alive_count
# =========================
# 绘制泡泡(强化视觉)
# =========================
def _draw_bubble(self, img, b):
# 呼吸动画
pulse = 1.0 + 0.15 * math.sin(self.frame_idx * 0.15 + b["phase"])
r = int(self.bubble_radius * pulse)
overlay = img.copy()
cx, cy = int(b["x"]), int(b["y"])
# 半透明填充
cv2.circle(overlay, (cx, cy), r, (255, 180, 80), -1)
cv2.addWeighted(overlay, 0.25, img, 0.75, 0, img)
# 外轮廓
cv2.circle(img, (cx, cy), r, (255, 220, 120), 3)
# 高光
cv2.circle(
img,
(cx - r // 3, cy - r // 3),
max(6, r // 6),
(255, 255, 255),
-1
)
# =========================
# 主执行函数(规范接口)
# =========================
def obj_exe(self, im0, tracks):
"""
Args:
im0 (ndarray): Image
tracks (list): YOLO tracking results
"""
self.im0 = im0
self.frame_idx += 1
h, w = im0.shape[:2]
if not tracks or not len(tracks[0]):
return self.im0
result = tracks[0]
if result.masks is None:
return self.im0
# ========= 分割掩码 =========
mask = result.masks.data[0].cpu().numpy()
mask = (mask * 255).astype(np.uint8)
if mask.shape != (h, w):
mask = cv2.resize(mask, (w, h))
# ========= 初始化 =========
if not self.initialized or len(self.bubbles) == 0:
self._generate_bubbles(mask, h, w)
self.initialized = True
# ========= 更新 =========
alive = self._update_bubbles(mask, h, w)
if alive == 0:
self._generate_bubbles(mask, h, w)
# ========= 绘制 =========
for b in self.bubbles:
if b["alive"]:
self._draw_bubble(self.im0, b)
elif b["boom"] > 0:
cv2.circle(
self.im0,
(int(b["x"]), int(b["y"])),
self.bubble_radius + (6 - b["boom"]) * 6,
(255, 255, 255),
2
)
return self.im0
七、总结
这个小项目本质上想表达一件事:
深度学习不只是“识别结果”,
它也可以是交互系统的一部分。
通过 YOLO 的分割掩码,我们可以非常低成本地构建:
-
碰撞区域
-
物理反馈
-
实时交互体验
如果你已经在用 YOLO 做检测或分割,
不妨试着把结果“玩”起来,而不仅仅是画个框。
对 PiscTrace or PiscCode感兴趣?更多精彩内容请移步官网看看~🔗 PiscTrace


被折叠的 条评论
为什么被折叠?



