用 YOLO Pose + Segmentation 在PiscCode构建“语义佛光”:一次实时视觉语义融合实验

在实时视觉系统中,检测结果往往只是"对的",却不"有感觉"

当我们开始关心"表现力"时,问题就变成:

能否让模型的语义本身,直接参与画面的构成?

本文记录一次基于 YOLO Pose + Segmentation 的实验:

我们将人体分割掩码、关键点骨架与径向衰减模型结合,构建了一种语义驱动的佛光(Halo)视觉效果


一、整体设计思路

系统目标很明确:

  1. 左侧:真实语义可视化

    • 人体分割区域

    • COCO 语义骨架(真实拓扑,不做美化)

  2. 右侧:语义增强表现

    • 以"鼻子 + 双肩"为语义锚点

    • 自动推导佛光半径

    • 使用连续衰减模型,而非高光或硬描边

最终输出为:

[ 原始帧 + 语义调试 ] | [ 语义佛光渲染 ]

这是一个调试友好 + 表现完整的结构。


二、YOLO 多模型并行推理设计

复制代码
model_paths=("yolo11n-pose.pt", "yolo11n-seg.pt")

这里并没有强行把任务塞进一个模型,而是选择:

模型 职责
YOLO-Pose 关键点 & 真实人体拓扑
YOLO-Seg 像素级语义边界

这有三个好处:

  1. 语义解耦:姿态 ≠ 轮廓

  2. 稳定性更好:Pose 丢点 ≠ Seg 失效

  3. 表现层更自由


三、分割掩码的工程处理细节

复制代码

def _mask_to_float(mask, shape):

这里非常关键的一点是:

  • 统一所有 mask 到 float32

  • 严格 resize 到原始图像空间

  • 不做模糊、不做插值

原因只有一个:

掩码是语义边界,不是特效素材

一旦模糊,语义就被破坏了。


四、真实语义骨架(不是美术骨架)

复制代码
self.SKELETON = [ (0, 1), (0, 2), (1, 3), (2, 4), ... ]

这不是"好看"的骨架,而是:

  • COCO 原始拓扑

  • 保留人体左右不对称

  • 保留真实关节关系

这样做的目的只有一个:

让所有后续视觉效果,都基于真实结构,而不是人为假设。


五、佛光的"半径"不是写死的

复制代码
def _calc_base_radius(nose, lsh, rsh):

佛光半径来源于:

复制代码
max( nose ↔ left shoulder, nose ↔ right shoulder, min_radius )

这意味着:

  • 人越近,佛光越大

  • 人越远,佛光越收敛

  • 不依赖 bbox(避免抖动)

这是一个完全语义驱动的尺度系统


六、为什么不用高光?而用径向衰减

复制代码
def _radial_disk(...): exp(- (dist / radius) ** gamma)

这是整个系统的核心哲学

你之前提到一句非常关键的话:

不应该是高光,而是前几帧慢慢透明

高光是能量叠加

而佛光应该是存在感衰减

径向指数衰减的优点:

  • 没有边界

  • 没有"亮点"

  • 中心稳定,外围自然消失

这使佛光更像一种状态场,而不是特效。


七、颜色不是一层,而是三层语义叠加

复制代码
gold_center * (a ** 0.35) gold_mid * (a ** 1.0) gold_outer * (a ** 2.2)

这是非常"反直觉"的地方:

  • 中心:指数最低 → 稳定

  • 中层:线性

  • 外围:指数最高 → 快速消失

这不是为了"金色",而是为了:

让佛光看起来像"存在",而不是"光源"。


八、为什么最后要用分割掩码"盖回人像"

复制代码
person = orig * M[..., None] out = out * (1 - M[..., None]) + person

这是为了一个非常重要的原则:

人不是特效的一部分

佛光可以包围人,但不能侵蚀人体语义

这一步保证:

  • 人体像素永远是原始真实的

  • 特效只存在于"人之外的空间"

    import cv2

    import numpy as np

    from ultralytics import YOLO

    class YOLORLTObject:

    复制代码
      def __init__(
    
          self,
    
          model_paths=("yolo11n-pose.pt", "yolo11n-seg.pt"),
    
          device="cuda",
    
      ):
    
          self.models = []
    
          self.model_types = []
    
    
    
          for p in model_paths:
    
              model = YOLO(p).to(device)
    
              self.models.append(model)
    
              if "pose" in p:
    
                  self.model_types.append("pose")
    
              elif "seg" in p:
    
                  self.model_types.append("seg")
    
              else:
    
                  self.model_types.append("det")
    
    
    
          self.device = device
    
    
    
          # COCO keypoints
    
          self.NOSE = 0
    
          self.L_SH = 5
    
          self.R_SH = 6
    
    
    
          # COCO skeleton (真实语义)
    
          self.SKELETON = [
    
              (0, 1), (0, 2),
    
              (1, 3), (2, 4),
    
    
    
              (5, 6),
    
    
    
              (5, 7), (7, 9),
    
              (6, 8), (8, 10),
    
    
    
              (5, 11), (6, 12),
    
              (11, 12),
    
    
    
              (11, 13), (13, 15),
    
              (12, 14), (14, 16)
    
          ]
    
    
    
      # =====================================================
    
      # 工具函数
    
      # =====================================================
    
    
    
      @staticmethod
    
      def _mask_to_float(mask, shape):
    
          m = mask.cpu().numpy()
    
          if m.dtype != np.uint8:
    
              m = (m > 0.5).astype(np.uint8)
    
          if m.shape[:2] != shape:
    
              m = cv2.resize(m, (shape[1], shape[0]), cv2.INTER_NEAREST)
    
          return m.astype(np.float32)
    
    
    
      @staticmethod
    
      def _calc_base_radius(nose, lsh, rsh, min_r=50):
    
          return max(
    
              np.linalg.norm(nose - lsh),
    
              np.linalg.norm(nose - rsh),
    
              min_r
    
          )
    
    
    
      @staticmethod
    
      def _radial_disk(h, w, center, radius, gamma=2.2):
    
          cx, cy = center
    
          y, x = np.ogrid[:h, :w]
    
          dist = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
    
          return np.clip(np.exp(- (dist / radius) ** gamma), 0, 1)
    
    
    
      # =====================================================
    
      # 左侧:真实语义骨架 + seg 可视化
    
      # =====================================================
    
    
    
      def _render_debug(self, frame, pose_res, seg_masks):
    
          vis = frame.copy()
    
    
    
          # --- SEG mask ---
    
          if seg_masks:
    
              M = np.maximum.reduce(seg_masks)
    
              overlay = np.zeros_like(vis)
    
              overlay[:] = (0, 255, 0)
    
              blended = cv2.addWeighted(vis, 0.75, overlay, 0.25, 0)
    
              vis[M > 0] = blended[M > 0]
    
    
    
          # --- Pose skeleton ---
    
          if pose_res is not None:
    
              for kp in pose_res.keypoints.data:
    
                  pts = kp[:, :2].cpu().numpy().astype(int)
    
    
    
                  # points
    
                  for x, y in pts:
    
                      if x > 0 and y > 0:
    
                          cv2.circle(vis, (x, y), 3, (0, 0, 255), -1)
    
    
    
                  # skeleton lines
    
                  for i, j in self.SKELETON:
    
                      if i < len(pts) and j < len(pts):
    
                          x1, y1 = pts[i]
    
                          x2, y2 = pts[j]
    
                          if x1 > 0 and y1 > 0 and x2 > 0 and y2 > 0:
    
                              cv2.line(vis, (x1, y1), (x2, y2), (255, 0, 0), 2)
    
    
    
          return vis
    
    
    
      # =====================================================
    
      # 右侧:佛光
    
      # =====================================================
    
    
    
      def _render_halo(self, frame, pose_res, seg_masks):
    
          H, W = frame.shape[:2]
    
          orig = frame.astype(np.float32)
    
          out = orig.copy()
    
    
    
          M = np.maximum.reduce(seg_masks)
    
    
    
          for kp in pose_res.keypoints.data:
    
              nose = kp[self.NOSE][:2].cpu().numpy()
    
              lsh = kp[self.L_SH][:2].cpu().numpy()
    
              rsh = kp[self.R_SH][:2].cpu().numpy()
    
    
    
              if nose.sum() == 0 or lsh.sum() == 0 or rsh.sum() == 0:
    
                  continue
    
    
    
              base_r = self._calc_base_radius(nose, lsh, rsh)
    
              radius = base_r * 2.0
    
    
    
              alpha = self._radial_disk(
    
                  H, W,
    
                  (int(nose[0]), int(nose[1])),
    
                  radius
    
              )
    
    
    
              a = alpha[..., None]
    
    
    
              gold_center = np.array([255, 250, 240], np.float32)
    
              gold_mid    = np.array([255, 220, 160], np.float32)
    
              gold_outer  = np.array([180, 120, 40],  np.float32)
    
    
    
              color = (
    
                  gold_center * (a ** 0.35) +
    
                  gold_mid    * (a ** 1.0) +
    
                  gold_outer  * (a ** 2.2)
    
              )
    
    
    
              out += color * a * 0.85
    
    
    
          # 人像复制覆盖
    
          person = orig * M[..., None]
    
          out = out * (1 - M[..., None]) + person
    
    
    
          return np.clip(out, 0, 255).astype(np.uint8)
    
    
    
      # =====================================================
    
      # 主入口
    
      # =====================================================
    
    
    
      def do(self, frame, device=None):
    
          if frame is None:
    
              return None
    
    
    
          device = device or self.device
    
          H, W = frame.shape[:2]
    
    
    
          pose_res = None
    
          seg_masks = []
    
    
    
          # 一次推理
    
          for i, model in enumerate(self.models):
    
              res = model.track(
    
                  frame,
    
                  persist=True,
    
                  verbose=False,
    
                  device=device
    
              )[0]
    
    
    
              if self.model_types[i] == "pose":
    
                  pose_res = res
    
              elif self.model_types[i] == "seg" and res.masks is not None:
    
                  for m in res.masks.data:
    
                      seg_masks.append(self._mask_to_float(m, (H, W)))
    
    
    
          if pose_res is None or not seg_masks:
    
              return frame
    
    
    
          left = self._render_debug(frame, pose_res, seg_masks)
    
          right = self._render_halo(frame, pose_res, seg_masks)
    
    
    
          return np.hstack([left, right])

九、这种结构的可扩展性

在这个框架下,你可以非常容易地加入:

  • 掉魂残影(Seg mask + 帧队列)

  • 气泡 / 粒子(mask 作为碰撞体)

  • 语义雾化 / 消散

  • 姿态驱动的场变化

关键不是效果,而是:语义永远在控制表现,而不是相反。


十、结语

这不是一个"好看滤镜"的实现,而是一次尝试:

把深度学习模型的中间语义,

直接当作视觉语言来使用。

当你不再把模型当作"黑箱",

而是当作实时语义生成器 时------

视觉系统的可能性,会变得非常大。

对 PiscTrace or PiscCode感兴趣?更多精彩内容请移步官网看看~🔗 PiscTrace

相关推荐
坐吃山猪2 小时前
Python命令行工具Click
linux·开发语言·python
nnerddboy2 小时前
解决传统特征波段选择的局限性:1.对偶学习
学习·算法·机器学习
初九之潜龙勿用2 小时前
GMM NZ 全流程详解实战:FSDP MOE 训练加速
人工智能·pytorch·python
山土成旧客2 小时前
【Python学习打卡-Day28】类的蓝图:从模板到对象的构建艺术
linux·python·学习
Mqh1807622 小时前
day47 预训练模型
python
CoovallyAIHub2 小时前
自顶向下 or 自底向上?姿态估计技术是如何进化的?
深度学习·算法·计算机视觉
乙酸氧铍2 小时前
python实现gif图片缩放,支持透明像素
图像处理·python·gif·pil
q_30238195562 小时前
14.7MB轻量模型!NVIDIA Jetson边缘设备解锁工厂设备故障预警新方案
人工智能·python·算法·ascend·算子开发
爱敲点代码的小哥2 小时前
C#哈希表遍历技巧全解析以及栈 堆 队列的认识
算法·哈希算法