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


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更近。
透视关系: 道路远端汇聚成一点。
相对大小: 同样是车,看着小的离得远。
深度图反直觉
生成的深度图中,越近的物体颜色越亮(数值越大),越远的地方颜色越黑(数值越小)。

交互式代码怎么读?
不要从上往下“逐行硬读”,效率很低。要按结构层级读。
一、先读这段程序想干什么
先只看最粗的骨架,不看细节。
这段程序的目标是:
- 加载一个深度估计模型
DPT - 加载一个分割模型
SAM - 读入一张图片
- 先对整张图算出深度图
- 再用鼠标点选目标,让
SAM分割出物体 - 用分割出的 mask 去深度图里取对应区域
- 计算这个物体区域的平均深度并打印
也就是一句话:用 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
意思是:
- 先把图片处理成模型输入
- 送进深度模型
- 拿到预测深度
然后:
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 主循环。
它不断做三件事:
- 复制原图
- 把点、mask、轮廓画上去
- 显示窗口并监听键盘
按键逻辑:
q:退出r:清空点和 mask,重新开始
四、真正该掌握的,不是代码细节,而是控制流
读这类代码最容易卡住的地方是:
以为程序是线性执行的,但其实它是半线性 + 事件驱动。
线性部分
程序启动时按顺序执行:
- 导入库
- 加载模型
- 调用
interactive_demo() - 读图
- 算深度图
- 设置 SAM 图像
- 创建窗口
- 进入
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 全图分割
- 再保存结果
这段代码可以按 主流程 + 两个任务 + 一个可视化函数 来读。
一、先一句话说清这段代码在干什么
这段代码的目标是:
对一张输入图片分别做两件事:
- 用
DPT做深度估计,输出深度图 - 用
SAM做自动分割,输出分割可视化图
最后保存两张结果图:
result_01_depth.pngresult_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:给SAM和matplotlib使用
读这段代码时要注意的点:不是重复读图,而是同一张图转成不同的数据格式,适配不同模型/库。
五、第一项任务:深度估计
这一段:
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_depth在depth模式下其实就是深度图矩阵。
情况 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 格式适合
SAM、matplotlib
3. 自动分割 vs 交互分割
这是和上一段最关键的对比:
SamPredictor:提示式/交互式SamAutomaticMaskGenerator:自动生成全图所有 mask
4. 可视化不等于算法本身
save_visualization() 只是“把结果画出来”,它不是模型推理本身。
读代码时一定要把:
- 算结果
- 显示结果
区分开。
十二、如果要自己分析这段代码,可以这样做笔记
可以在代码旁边写出这种注释:
# 主流程:
# 1. 读入图片
# 2. 跑 DPT 深度估计,保存深度图
# 3. 跑 SAM 自动分割,保存分割叠加图
再给每个块打标签:
# [模型准备]
# [输入读取]
# [任务1:深度估计]
# [任务2:SAM自动分割]
# [结果可视化]

浙公网安备 33010602011771号