用 YOLO 分割掩码在PiscTrace内做一个「会被戳爆的泡泡」交互特效

在很多计算机视觉项目中,目标检测或分割的结果往往只用于“显示框”或“统计数据”。但如果我们换一个思路:
👉 把视觉模型的输出当作“交互边界”,是不是就能做出一些更有趣的实时效果?

这篇文章分享一个基于 YOLO 实例分割(Segmentation Mask) 的小实验:
画面中会漂浮一些泡泡,当泡泡碰到被分割出来的目标区域时,就会“爆炸”。


一、为什么选择 YOLO + Segmentation?

1. YOLO 不只是检测框

大多数人对 YOLO 的印象是这样的:

  • 输入一帧图像

  • 输出若干个 bbox + class + confidence

但从 YOLOv8 开始,YOLO 在实时性不变的前提下,已经原生支持了:

  • 实例分割(Instance Segmentation)

  • 每个目标都附带一个 像素级掩码(Mask)

这让 YOLO 非常适合做 实时交互型视觉应用,例如:

  • 人体轮廓交互

  • AR / 特效遮挡

  • 基于区域的物理反馈


2. Segmentation Mask 的价值

相比 bounding box,分割掩码有三个关键优势:

  1. 像素级精度
    不是“框住一个人”,而是精确到轮廓

  2. 天然的交互边界
    掩码本身就是一个二维碰撞区域

  3. 无需额外后处理
    不需要复杂的几何计算,直接查像素即可

在本文的代码中,掩码被直接转成了一个 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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值