Task02 视觉感知、仿真环境与训练范式入门-04-具身场景的计算机视觉、3D重建/01-sam和深度估计

1008fae7330f2f22299c8c1cc4ee5f32
357445f2d8e4df2ac6b9037917090e8a

04-具身场景的计算机视觉、3D重建/01-sam和深度估计

Segment Anything Model (SAM)

图像编码器 (Image Encoder): 基于 ViT-H (Vision Transformer Huge),负责对图像进行深度特征提取。这是最耗时的部分,但一张图只需要运行一次。
提示编码器 (Prompt Encoder): 将用户的点击(点)、框选(Box)或语言描述转化为向量。
掩码解码器 (Mask Decoder): 极其轻量级。结合图像特征和提示特征,毫秒级生成分割掩码。这就是为什么上面的交互式 Demo 可以做到实时响应。

单目深度估计 (Monocular Depth Estimation),DPT (Dense Prediction Transformer)

单目(一个摄像头),模型无法像双目相机那样通过视差计算深度。
所以DPT需要学习上下文线索!
遮挡关系: 物体A挡住物体B,A更近。
透视关系: 道路远端汇聚成一点。
相对大小: 同样是车,看着小的离得远。

深度图反直觉

生成的深度图中,越近的物体颜色越亮(数值越大),越远的地方颜色越黑(数值越小)。
image

交互式代码怎么读?

不要从上往下“逐行硬读”,效率很低。要按结构层级读。

一、先读这段程序想干什么

先只看最粗的骨架,不看细节。
这段程序的目标是:

  1. 加载一个深度估计模型 DPT
  2. 加载一个分割模型 SAM
  3. 读入一张图片
  4. 先对整张图算出深度图
  5. 再用鼠标点选目标,让 SAM 分割出物体
  6. 用分割出的 mask 去深度图里取对应区域
  7. 计算这个物体区域的平均深度并打印
    也就是一句话:用 SAM 交互式选物体,再结合 DPT 深度图,得到该物体的相对深度。

二、第二遍读模块分区

这段代码已经写了分区注释,其实作者已经在帮你读了。

1. 导入库

import numpy as np
import torch
import cv2
...

这里先不要深究 API,只判断每个库大概负责什么:

  • numpy:数组处理
  • torch:深度学习推理
  • cv2:图像显示、鼠标交互、画点画轮廓
  • segment_anything:SAM 分割
  • transformers:DPT 深度模型
  • PIL.Image:把 numpy 图像转成 PIL 格式给深度模型
  • os:检查文件路径
    你只要知道谁干什么,先别被细节绊住。

2. 配置区

SAM_CHECKPOINT = "sam_vit_h_4b8939.pth" 
MODEL_TYPE = "vit_h"
DEPTH_MODEL_NAME = "Intel/dpt-large"
IMAGE_PATH = "test_image.jpg"

配置区通常回答四个问题:

  • 模型文件在哪
  • 模型类型是什么
  • 输入数据在哪
  • 用 CPU 还是 GPU
    这是程序的外部参数入口。

3. 模型加载区

depth_processor = DPTImageProcessor.from_pretrained(DEPTH_MODEL_NAME)
depth_model = DPTForDepthEstimation.from_pretrained(DEPTH_MODEL_NAME).to(device)
...
sam = sam_model_registry[MODEL_TYPE](checkpoint=SAM_CHECKPOINT)
predictor = SamPredictor(sam)

这一段的任务只有一个:把后面要用的工具准备好。
这里不需要一上来就懂 from_pretrained()sam_model_registry 的实现。
只要先记住:

  • depth_processor:负责把输入图片处理成 DPT 能吃的格式
  • depth_model:负责预测深度
  • predictor:负责基于点击点做 SAM 分割

三、第三遍读函数级逻辑

代码里最关键的是两个函数:

函数 1:get_depth_map(image_pil)

它只做一件事:
输入一张图,输出这张图的深度图。
可以按“输入 → 处理 → 输出”读:

输入
def get_depth_map(image_pil):

传入的是 PIL 格式的图片。

处理
inputs = depth_processor(images=image_pil, return_tensors="pt").to(device)
outputs = depth_model(**inputs)
predicted_depth = outputs.predicted_depth

意思是:

  1. 先把图片处理成模型输入
  2. 送进深度模型
  3. 拿到预测深度
    然后:
prediction = torch.nn.functional.interpolate(...)

因为模型输出分辨率通常比原图小,所以这里把深度图插值回原图尺寸。

输出
return prediction.squeeze().cpu().numpy()

最后转成 numpy 数组,方便后面直接索引。
所以这个函数可以浓缩成一句:
把图片送进 DPT,返回与原图同尺寸的深度矩阵。

函数 2:interactive_demo()

这是主流程函数。
应该把它拆成 5 步读:

第一步:读图

if not os.path.exists(IMAGE_PATH):
    ...
image_cv = cv2.imread(IMAGE_PATH)
image_cv = cv2.resize(...)
image_rgb = cv2.cvtColor(...)
image_pil = Image.fromarray(image_rgb)

这里做的是:

  • 检查图片是否存在
  • 用 OpenCV 读图
  • 缩放一下
  • BGR 转 RGB
  • 转成 PIL,给深度模型用

第二步:预计算深度图

depth_map = get_depth_map(image_pil)

这句很重要。说明深度不是点击后每次重新算,而是先整张图一次性算好。
这样交互的时候速度更快。

第三步:给 SAM 设置当前图像

predictor.set_image(image_rgb)

意思是把当前图喂给 SAM,后面点击点时,SAM 就基于这张图分割。

第四步:注册鼠标回调

def mouse_callback(event, x, y, flags, param):

这是整段代码最值得重点读的地方。
因为交互程序的核心不是“从上往下执行”,而是“事件驱动”。
点击鼠标时,程序才会进入这个函数。
它的逻辑是:

A. 记录点击点
if event == cv2.EVENT_LBUTTONDOWN:
    input_point.append([x, y])
    input_label.append(1)
elif event == cv2.EVENT_RBUTTONDOWN:
    input_point.append([x, y])
    input_label.append(0)
  • 左键:前景点
  • 右键:背景点
B. 用点击点调用 SAM
masks, scores, _ = predictor.predict(
    point_coords=np.array(input_point),
    point_labels=np.array(input_label),
    multimask_output=False
)
current_mask = masks[0]

意思是:根据当前累计的前景/背景提示点,重新预测一个 mask。

C. 用 mask 去深度图中取值
masked_depth = depth_map[current_mask]
avg_depth = np.mean(masked_depth)

这一句是这段程序最核心的融合点。

  • depth_map:整张图每个像素的深度
  • current_mask:哪些像素属于当前物体
  • depth_map[current_mask]:把物体区域的深度全部取出来
  • np.mean(...):算平均深度
    这就把分割结果和深度结果结合起来了。

第五步:进入显示循环

while True:
    ...
    cv2.imshow(...)
    key = cv2.waitKey(50) & 0xFF

这里是 GUI 主循环。
它不断做三件事:

  1. 复制原图
  2. 把点、mask、轮廓画上去
  3. 显示窗口并监听键盘
    按键逻辑:
  • q:退出
  • r:清空点和 mask,重新开始

四、真正该掌握的,不是代码细节,而是控制流

读这类代码最容易卡住的地方是:
以为程序是线性执行的,但其实它是半线性 + 事件驱动。

线性部分

程序启动时按顺序执行:

  1. 导入库
  2. 加载模型
  3. 调用 interactive_demo()
  4. 读图
  5. 算深度图
  6. 设置 SAM 图像
  7. 创建窗口
  8. 进入 while True

事件驱动部分

进入循环以后,主要执行流变成:

  • 程序等鼠标点击
  • 一点击,就触发 mouse_callback
  • 回调里重新分割并计算平均深度
  • 然后显示循环继续刷新窗口
    所以要把代码分成:
  • 启动流程
  • 交互事件流程
    这类程序一旦这么读,就顺了。

五、一个实用阅读模板

以后看到类似代码,按这个顺序读:

第一步:看入口

if __name__ == "__main__":
    interactive_demo()

先找程序从哪进。

第二步:看主函数

这里就是 interactive_demo()
先弄清主函数在做哪些阶段性动作,不要先读内部每个 API。

第三步:看辅助函数

这里是 get_depth_map()
判断它的职责,不要陷进实现细节。

第四步:看状态变量

input_point = []
input_label = []
current_mask = None

这是交互程序的状态中心。
看到状态变量,你就知道程序运行过程中“什么会变化”。
这里变化的是:

  • 点集合
  • 点标签
  • 当前 mask

第五步:看事件回调

def mouse_callback(...)

凡是 GUI、前端、机器人控制、游戏循环,回调几乎都是重点。

第六步:看显示/刷新循环

while True:

看它每轮怎么刷新画面,怎么处理退出、重置等输入。

六、把这段代码压缩成伪代码

可以试着把原程序在脑子里压缩成这个版本:

加载深度模型
加载 SAM 模型
读入图片
计算整张图的深度图
把图片设置给 SAM
初始化:点击点列表、标签列表、当前mask
当鼠标点击时:
    记录点和前景/背景标签
    用这些点让 SAM 重新分割
    得到当前mask
    从深度图中取出 mask 区域
    计算平均深度并打印
循环显示:
    画点击点
    画 mask 半透明叠加
    画轮廓
    监听 q 退出,r 重置

如果能把代码压到这个程度,说明已经读懂 80% 了。

七、这段代码里最值得重点理解的 3 个知识点

1. 模型职责分离

  • DPT:给全图深度
  • SAM:给目标区域
  • 两者结合:得到目标深度

2. mask 索引

masked_depth = depth_map[current_mask]

这是 numpy 很典型的布尔索引。
current_mask 是一个布尔矩阵,True 的地方就被取出来。

3. 事件驱动

不是每次循环都重新做分割,而是只有点击时才做。

八、给你的具体练习方法

可以按这 3 步练:

练习 1:手写注释版

在每段代码上面写一句自己的话,例如:

# 先算整图深度,避免每次点击都重复推理
depth_map = get_depth_map(image_pil)

练习 2:画流程图

画成这样:

  • 启动
  • 加载模型
  • 读图
  • 算深度
  • SAM 设置图像
  • 等点击
  • 点击后分割
  • 从深度图取 mask 区域
  • 计算平均深度
  • 刷新显示

练习 3:删细节保主干

把代码临时缩成 30 行伪代码,只保留主流程。
这是最快的读代码方法。

九、如果你是初学者,最容易误解的地方

误区 1:以为 while True 里才是全部逻辑

不是。真正的业务关键一半在 mouse_callback 里。

误区 2:以为 depth_map 是点击后才算

不是。它在交互前就一次性算好了。

误区 3:以为 avg_depth 是真实物理距离

不是。这里通常是相对深度,不是直接米制距离。
Intel/dpt-large 给的是单目深度估计结果,更多是相对远近关系,不是严格标定后的绝对深度。
最后把这段代码按“模块—函数—事件—状态”四层自己复述一遍。复述不出来的那一层,就是下一步该补的点。

脚本代码怎么读?

这段代码和你上一段相比,更容易读,因为它不是交互式的,而是一个标准的顺序脚本:

  • 读图
  • 跑深度估计
  • 保存结果
  • 跑 SAM 全图分割
  • 再保存结果
    这段代码可以按 主流程 + 两个任务 + 一个可视化函数 来读。

一、先一句话说清这段代码在干什么

这段代码的目标是:
对一张输入图片分别做两件事:

  1. DPT 做深度估计,输出深度图
  2. SAM 做自动分割,输出分割可视化图
    最后保存两张结果图:
  • result_01_depth.png
  • result_02_sam_seg.png

二、先看整体结构

这段代码可以拆成 4 块:

第 1 块:导入依赖

import torch
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import os
import time

还有模型相关:

from transformers import DPTImageProcessor, DPTForDepthEstimation
from segment_anything import sam_model_registry, SamAutomaticMaskGenerator

还是先只看职责:

  • torch:跑模型
  • numpy:图像数组处理
  • PIL.Image:读图
  • matplotlib:画图、保存结果
  • os:检查路径
  • transformers:DPT 深度估计
  • segment_anything:SAM 自动分割

第 2 块:设备选择

device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"使用设备: {device}")

作用很简单:

  • 有 GPU 就用 GPU
  • 没有就用 CPU
    这通常是深度学习脚本的固定模板。

第 3 块:辅助函数 save_visualization(...)

这是一个结果展示/保存函数
它的作用是:根据模式不同,把结果可视化并保存成图片。
这里有两个模式:

  • mode="depth":保存深度图
  • mode="sam":保存 SAM 分割叠加图

第 4 块:主函数 main()

这是程序入口,负责整体流程。
最后用:

if __name__ == "__main__":
    main()

启动程序。

三、先读主函数 main(),不要一开始就钻进细节

这是读代码最关键的习惯。

main() 的整体流程

可以把它压缩成这样:

设置图片路径和 SAM 权重路径
检查图片是否存在
加载图片

运行深度估计
保存深度图

运行 SAM 全图分割
保存分割图

如果能先读出这个层级,后面细节就好理解了。

四、逐段读 main()

1. 路径设置

rgb_path = "image_d61af3.jpg"
sam_ckpt = "sam_vit_h_4b8939.pth"

这两行定义了两个输入:

  • rgb_path:待处理图片
  • sam_ckpt:SAM 权重文件

2. 检查图片是否存在

if not os.path.exists(rgb_path):
    print(f"错误: 找不到图片 {rgb_path}")
    return

意思是:

  • 如果输入图片不存在,就直接退出程序
    这是脚本里很常见的保护性检查。

3. 加载图片

image_pil = Image.open(rgb_path).convert("RGB")
image_np = np.array(image_pil)

这里同一张图被转成了两种格式:

  • image_pil:给 DPT 模型使用
  • image_np:给 SAMmatplotlib 使用
    读这段代码时要注意的点:不是重复读图,而是同一张图转成不同的数据格式,适配不同模型/库。

五、第一项任务:深度估计

这一段:

print("\n--- [1/2] 正在运行深度估计 ---")
try:
    depth_processor = DPTImageProcessor.from_pretrained("Intel/dpt-large")
    depth_model = DPTForDepthEstimation.from_pretrained("Intel/dpt-large").to(device)
    
    inputs = depth_processor(images=image_pil, return_tensors="pt").to(device)
    with torch.no_grad():
        outputs = depth_model(**inputs)
        predicted_depth = outputs.predicted_depth

可以按“加载 → 预处理 → 推理”来读。

1) 加载模型

depth_processor = DPTImageProcessor.from_pretrained("Intel/dpt-large")
depth_model = DPTForDepthEstimation.from_pretrained("Intel/dpt-large").to(device)

含义:

  • depth_processor:负责把图片处理成模型输入格式
  • depth_model:负责做深度预测

2) 预处理输入

inputs = depth_processor(images=image_pil, return_tensors="pt").to(device)

作用:

  • PIL 图片转成 PyTorch Tensor
  • 再放到 CPU/GPU 上

3) 推理

with torch.no_grad():
    outputs = depth_model(**inputs)
    predicted_depth = outputs.predicted_depth

意思是:

  • 在不计算梯度的推理模式下跑模型
  • 输出深度预测结果
    predicted_depth 是模型原始输出。

4) 恢复到原图尺寸

prediction = torch.nn.functional.interpolate(
    predicted_depth.unsqueeze(1),
    size=image_pil.size[::-1],
    mode="bicubic",
    align_corners=False,
).squeeze().cpu().numpy()

这一段可以这样理解:模型输出的深度图尺寸通常和原图不一样,所以这里做了:

  • 增加一个通道维度 unsqueeze(1)
  • 双三次插值到原图尺寸
  • 去掉多余维度 squeeze()
  • 从 GPU 搬回 CPU
  • 转成 numpy
    最后 prediction 就是一张和原图尺寸一致的深度图矩阵。

5) 保存结果

save_visualization(image_np, prediction, mode="depth", output_name="result_01_depth.png")

这句意思是:

  • 把原图和深度图并排画出来
  • 保存为 result_01_depth.png

6) 异常处理

except Exception as e:
    print(f"深度估计失败: {e}")

如果深度模型报错,就打印错误,但程序不会直接崩掉。

六、第二项任务:SAM 全图分割

这一段:

print("\n--- [2/2] 正在运行 SAM 分割 ---")
if os.path.exists(sam_ckpt):
    try:
        sam = sam_model_registry["vit_h"](checkpoint=sam_ckpt).to(device)
        mask_generator = SamAutomaticMaskGenerator(sam)
        masks = mask_generator.generate(image_np)
        
        save_visualization(image_np, masks, mode="sam", output_name="result_02_sam_seg.png")

这部分也按“加载 → 生成 mask → 保存结果”来读。

1) 先检查权重是否存在

if os.path.exists(sam_ckpt):

意思是:

  • 如果 sam_vit_h_4b8939.pth 存在,就执行 SAM
  • 否则跳过
    这比上一段代码更稳一点,因为它给了“可跳过”的逻辑。

2) 加载 SAM

sam = sam_model_registry["vit_h"](checkpoint=sam_ckpt).to(device)

这里创建了一个 vit_h 版本的 SAM 模型。

3) 创建自动分割器

mask_generator = SamAutomaticMaskGenerator(sam)

这和你上一段代码不一样。
上一段你用的是:

  • SamPredictor
    那是交互式点击分割,需要人点前景/背景。
    这一段用的是:
  • SamAutomaticMaskGenerator
    它是自动全图分割,不需要点击,会自动找图中多个区域并生成多个 mask。
    这是这两段代码最核心的差异之一。

4) 生成分割结果

masks = mask_generator.generate(image_np)

这会返回一组 mask,不是一个单独的 mask。
通常 masks 是一个列表,每个元素像这样:

{
    'segmentation': ...,
    'area': ...,
    ...
}

也就是说:

  • segmentation:布尔掩码
  • area:掩码面积
  • 还有其他信息

5) 保存分割结果

save_visualization(image_np, masks, mode="sam", output_name="result_02_sam_seg.png")

作用:

  • 在原图上把所有 mask 用半透明颜色叠加显示
  • 保存成图像

6) 找不到权重时跳过

else:
    print(f"跳过 SAM: 未找到权重文件 {sam_ckpt}")

说明这个程序设计得比较稳:

  • SAM 没法跑,不影响前面的深度估计

七、这段代码里最重要的函数:save_visualization(...)

这是你真正应该重点读的辅助函数。
它根据 mode 不同,有两种逻辑。

情况 1:mode == "depth"

plt.subplot(1, 2, 1)
plt.imshow(image)
plt.title("Original Image")
plt.axis('off')

plt.subplot(1, 2, 2)
plt.imshow(mask_or_depth, cmap="inferno")
plt.colorbar(label="Relative Depth")
plt.title("Depth Estimation")
plt.axis('off')

意思:

  • 左边画原图
  • 右边画深度图
  • inferno 颜色映射显示深度
  • 加 colorbar 表示深度值大小规律
    这里 mask_or_depthdepth 模式下其实就是深度图矩阵。

情况 2:mode == "sam"

这一段稍复杂一点:

plt.imshow(image)
ax = plt.gca()
ax.set_autoscale_on(False)

先显示原图。
然后:

sorted_anns = sorted(mask_or_depth, key=(lambda x: x['area']), reverse=True)

对所有 mask 按面积从大到小排序。
这样做的目的:

  • 大区域先画
  • 小区域后画
  • 显示叠加时更自然,不容易被大块遮住小块
    然后:
img_overlay = np.ones((h, w, 4))
img_overlay[:,:,3] = 0

创建一张 RGBA 透明图层。

  • 前 3 维是颜色
  • 第 4 维是透明度 alpha
    初始透明度全为 0,也就是完全透明。
    接着对每个 mask:
for ann in sorted_anns:
    m = ann['segmentation']
    color_mask = np.concatenate([np.random.random(3), [0.4]])
    img_overlay[m] = color_mask

意思是:

  • 取出当前区域的布尔 mask
  • 随机生成一个 RGB 颜色
  • 再附加透明度 0.4
  • 把这个颜色填充到 mask 区域
    最后:
ax.imshow(img_overlay)

把半透明叠加层显示在原图上。

最后统一保存

plt.savefig(output_name, bbox_inches='tight')
plt.close()
  • 保存图片
  • 关闭图窗,释放资源

八、这段代码和上一段代码的本质区别

现在最该学会的是比较阅读。

上一段代码:交互式

特点:

  • 用户点击前景/背景点
  • SAM 根据点击点分割一个目标
  • 再算该目标的平均深度
  • 有窗口交互,按键、鼠标回调
    核心是:交互式选中一个物体并分析它。

这一段代码:批处理式

特点:

  • 不需要用户交互
  • 自动对整张图做深度估计
  • 自动对整张图做全图分割
  • 直接保存结果图
    核心是:对整张图跑两个视觉任务,并输出结果。

九、应该怎么读这类“脚本型代码”

这类代码比交互式代码简单,可以按下面方法读。

第一步:找入口

if __name__ == "__main__":
    main()

看到这里就知道:程序从 main() 开始。

第二步:看 main() 的任务切分

这段代码作者其实已经帮你切好了:

# 任务 1: 深度估计
# 任务 2: SAM 全图分割

读代码时优先看这种任务块。

第三步:看辅助函数在干嘛

这里只有一个辅助函数:

save_visualization(...)

它不参与模型推理,只负责展示和保存
所以读代码时,要能区分:

  • 哪部分是“核心算法”
  • 哪部分是“工程包装”
    这里:
  • 模型加载、推理 = 核心算法
  • save_visualization = 结果展示

第四步:看数据是怎么流动的

可以把数据流画成这样:

输入图片

rgb_path

读图

image_pil
image_np

分两路
路线 A:深度估计

image_pil
depth_processor
depth_model
predicted_depth
→ 插值
prediction
save_visualization

路线 B:SAM 分割

image_np
SAM
mask_generator.generate
masks
save_visualization
这就是数据流阅读法。

十、把这段代码压成伪代码

如果能自己把它压缩成下面这样,就说明读懂了:

选择设备

定义保存结果的函数:
    如果是深度图:
        原图和深度图并排画出来
    如果是 SAM 分割:
        把所有 mask 用半透明颜色叠加到原图上
    保存图片

主函数:
    设置输入图路径和 SAM 权重路径
    检查图片是否存在
    读取图片

    运行 DPT 深度估计
    把深度图保存为图片

    如果有 SAM 权重:
        运行 SAM 自动分割
        把分割叠加图保存为图片
    否则跳过

十一、这段代码最值得掌握的 4 个点

1. try/except 的作用

try:
    ...
except Exception as e:
    ...

这说明程序作者希望:

  • 深度估计失败时,程序不要整个崩掉
  • SAM 失败时,也不要整个崩掉
    这是工程里常见的容错处理。

2. 一图多格式

image_pil = Image.open(...)
image_np = np.array(image_pil)

同一张图可以有不同表示方式:

  • PIL 格式适合某些模型接口
  • numpy 格式适合 SAMmatplotlib

3. 自动分割 vs 交互分割

这是和上一段最关键的对比:

  • SamPredictor:提示式/交互式
  • SamAutomaticMaskGenerator:自动生成全图所有 mask

4. 可视化不等于算法本身

save_visualization() 只是“把结果画出来”,它不是模型推理本身。
读代码时一定要把:

  • 算结果
  • 显示结果
    区分开。

十二、如果要自己分析这段代码,可以这样做笔记

可以在代码旁边写出这种注释:

# 主流程:
# 1. 读入图片
# 2. 跑 DPT 深度估计,保存深度图
# 3. 跑 SAM 自动分割,保存分割叠加图

再给每个块打标签:

# [模型准备]
# [输入读取]
# [任务1:深度估计]
# [任务2:SAM自动分割]
# [结果可视化]
posted @ 2026-04-23 13:53  asandstar  阅读(42)  评论(0)    收藏  举报