在计算机视觉领域,光流估计一直是个经典而富有挑战性的问题。简单来说,光流就是描述视频中相邻两帧之间像素运动的技术。想象一下,你正在观看一场足球比赛直播,当球员跑动时,电视画面会呈现出流畅的运动轨迹——这种运动信息的捕捉与呈现,本质上就是光流技术的应用场景。
传统的光流算法如Lucas-Kanade、Horn-Schunck等,虽然在特定场景下表现不错,但面对复杂光照变化、快速运动或遮挡等情况时往往力不从心。2015年,随着深度学习在计算机视觉领域的爆发式发展,研究人员开始尝试用神经网络来解决这个经典问题,由此诞生了FlowNet——第一个基于CNN的光流估计网络。
提示:光流估计的难点在于它是个典型的"病态问题"——给定两帧图像,理论上存在无数种运动场都能解释像素的变化。因此,如何设计网络结构来学习合理的运动先验至关重要。
FlowNet提出了两种基础架构变体,分别针对不同的输入处理策略:
FlowNetS(Simple):直接将两帧图像在通道维度拼接(6通道输入),让网络自行学习如何比较两幅图像。这种设计简单粗暴,但需要网络从头学习比较策略。
FlowNetCorr:采用双分支结构分别处理两帧图像,然后通过专门的"相关层"(Correlation Layer)显式计算局部匹配。相关层的计算方式如下:
python复制def correlation(feat1, feat2, max_displacement=40):
"""计算两个特征图之间的局部相关性"""
b, c, h, w = feat1.shape
feat1 = F.pad(feat1, (max_displacement,)*4)
feat2 = F.pad(feat2, (max_displacement,)*4)
correlation_tensor = []
for i in range(2 * max_displacement + 1):
for j in range(2 * max_displacement + 1):
shifted = feat2[:, :, i:i+h, j:j+w]
correlation_tensor.append(torch.mean(feat1 * shifted, dim=1))
return torch.stack(correlation_tensor, dim=1)
FlowNet采用类似U-Net的编解码结构:
这种设计在当时非常新颖,因为传统方法通常只在单一尺度上计算光流。多尺度处理能有效捕捉不同大小的运动。
FlowNet采用多尺度端点误差(EPE)作为损失函数:
$$
\mathcal{L} = \sum_{s=1}^S \gamma^{S-s} | \mathbf{f}s - \mathbf{f} |_2
$$
其中$S=4$表示4个解码器阶段,$\gamma=0.8$控制不同尺度的权重。这种设计迫使网络在早期阶段就学习合理的运动估计,而不是依赖后面的上采样来修正错误。
RAFT(Recurrent All-Pairs Field Transforms)在2020年横空出世,一举成为光流估计的新标杆。其核心创新在于:
RAFT使用两个共享权重的编码器:
python复制class FeatureEncoder(nn.Module):
def __init__(self):
super().__init__()
self.conv_layers = nn.Sequential(
nn.Conv2d(3, 64, 7, stride=2, padding=3),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, 3, stride=1, padding=1),
nn.ReLU(inplace=True),
# 共6个残差块...
)
def forward(self, img):
return self.conv_layers(img)
与传统局部相关不同,RAFT计算全图所有像素对的相似度:
$$
C_{ijkl} = \sum_h f^1_{ijh} \cdot f^2_{klh}
$$
这会产生一个H×W×H×W的4D张量,通过池化构建多尺度金字塔。
GRU单元的结构如下:
python复制class ConvGRU(nn.Module):
def __init__(self, hidden_dim=128, input_dim=192+128):
super().__init__()
self.convz = nn.Conv2d(hidden_dim+input_dim, hidden_dim, 3, padding=1)
self.convr = nn.Conv2d(hidden_dim+input_dim, hidden_dim, 3, padding=1)
self.convq = nn.Conv2d(hidden_dim+input_dim, hidden_dim, 3, padding=1)
def forward(self, h, x):
hx = torch.cat([h, x], dim=1)
z = torch.sigmoid(self.convz(hx))
r = torch.sigmoid(self.convr(hx))
q = torch.tanh(self.convq(torch.cat([r*h, x], dim=1)))
return (1-z) * h + z * q
每个迭代步骤都会产生一个光流增量$\Delta f$,逐步优化预测结果。
| 特性 | RAFT | RAFT-S |
|---|---|---|
| 参数量 | 5.3M | 1.0M |
| 特征维度 | 256 | 128 |
| GRU单元 | 2个(3x3和1x1) | 1个(3x3) |
| 推理速度(FPS) | 15 | 60 |
| Sintel(clean) EPE | 1.43 | 1.76 |
RAFT-S通过减少特征维度和简化GRU结构,实现了4倍的加速,仅损失少量精度。
首先安装必要依赖:
bash复制conda create -n raft python=3.8
conda activate raft
pip install torch torchvision opencv-python matplotlib
git clone https://github.com/princeton-vl/RAFT.git
cd RAFT
python复制import torch
from raft import RAFT
def load_model(args):
model = RAFT(args)
if args.cuda:
model = nn.DataParallel(model)
model.load_state_dict(torch.load(args.model))
model.cuda()
else:
# CPU模式需要移除DataParallel引入的'module.'前缀
model.load_state_dict({k.replace('module.',''):v
for k,v in torch.load(args.model).items()})
return model.eval()
python复制def run_inference(model, video_path):
cap = cv2.VideoCapture(video_path)
ret, prev_frame = cap.read()
prev_tensor = preprocess(prev_frame)
with torch.no_grad():
while True:
ret, curr_frame = cap.read()
if not ret: break
curr_tensor = preprocess(curr_frame)
# 20次迭代优化
_, flow_up = model(prev_tensor, curr_tensor, iters=20)
visualize(prev_frame, flow_up)
prev_tensor = curr_tensor
RAFT提供了将光流转换为彩色图像的工具:
python复制from raft.utils.flow_viz import flow_to_image
def visualize(img, flow):
flow_img = flow_to_image(flow.permute(1,2,0).cpu().numpy())
combined = np.vstack([img, flow_img])
cv2.imshow('Optical Flow', combined)
cv2.waitKey(30)
端点误差(EPE)是光流估计的标准评测指标:
$$
EPE = \sqrt{(u_{pred}-u_{gt})^2 + (v_{pred}-v_{gt})^2}
$$
| 方法 | Sintel(clean) | Sintel(final) | KITTI(EPE) | KITTI(F1) |
|---|---|---|---|---|
| FlowNet2 | 2.02 | 3.54 | 4.09 | 11.8% |
| PWC-Net | 2.55 | 3.93 | 4.14 | 10.4% |
| RAFT | 1.43 | 2.71 | 3.12 | 5.4% |
| RAFT-S | 1.76 | 3.18 | 3.85 | 7.1% |
问题现象:处理高分辨率视频时出现OOM错误
解决方案:
--small参数切换到RAFT-Spython复制def resize_for_raft(img, max_size=1024):
h, w = img.shape[:2]
if max(h,w) > max_size:
scale = max_size / max(h,w)
return cv2.resize(img, (int(w*scale), int(h*scale)))
return img
问题现象:快速运动导致光流断裂
改进策略:
python复制def temporal_loss(flow1, flow2):
"""鼓励相邻光流场保持时间连续性"""
warped_flow1 = warp_flow(flow1, flow2)
return torch.mean(torch.abs(warped_flow1 - flow1))
问题现象:物体边缘光流不清晰
改进方法:
python复制def edge_aware_loss(flow, image):
"""利用图像边缘指导光流平滑"""
grad_x = image[:,:,1:] - image[:,:,:-1]
grad_y = image[:,1:,:] - image[:,:-1,:]
flow_grad_x = flow[:,:,1:] - flow[:,:,:-1]
flow_grad_y = flow[:,1:,:] - flow[:,:-1,:]
return torch.exp(-grad_x.abs()).mean() * flow_grad_x.abs().mean() + \
torch.exp(-grad_y.abs()).mean() * flow_grad_y.abs().mean()
在实现这些高级应用时,我发现调整迭代次数对结果质量影响显著。对于一般视频分析,10-15次迭代通常足够;但对于需要亚像素精度的任务如视频插帧,建议增加到30次以上。另一个实用技巧是在计算相关体积时适当限制最大位移范围,这能显著降低内存消耗而不明显影响质量。