[Paper Review] SmoothNet, A Plug-and-Play Network for Refining Human Poses in Videos
이번에 읽을 논문은 ECCV 2022에서 발표 예정인 SmoothNet 이라는 논문 입니다. 제가 개인적으로 Plug-and-Play Network를 굉장히 좋아합니다 ㅎㅎㅎㅎ 제목부터 마음에 드네요.
Abstract
내용을 간단히 소개하자면 human motion video 환경에서 기존 pose estimation 방식들의 output jitter들은 프레임 전반에 걸쳐 다양한 estimation error를 발생시키는데, 거의 보이지 않거나 가려진 동작의 경우 여러 관절의 추정 위치가 연속 프레임 시퀀스에 대한 실제 값에서 크게 벗어나 significant jitter들을 발생시키게 됩니다. 이러한 문제를 해결하기 위해 SmoothNet을 제안했습니다.
본 논문에서는 jitter를 완화하기 위해 기존 pose estimator에 dedicated temporal only refinement network를 연결할 것을 제안합니다. 즉, 모든 joint의 long-range temporal relation을 학습하여 body movement의 natural smoothness characteristics을 모델링 하도록 합니다. 단순하지만 효과적인 motion-aware fully-connected network를 통해 SmoothNet은 기존 pose estimation 방법들의 temporal smoothness를 크게 개선 시키고, 어려운 프레임의 estimation accuracy를 향상 시키게 됩니다.
위 그림에서 (a)는 제안된 개선 방법을 사용한 2D, 3D human pose estimation 및 body recovery의 전체 프레임워크를 나타내고, 제안된 SmoothNet은 estimation된 결과를 부드럽게 만들어주는 plug-and-play refinement network입니다. 그림 (b)는 SOTA인 RLE(Residual Log-likelihood Estimation) + SmoothNet의 조합의 결과를 나타내는데, 그래프에서 위에 있는 그래프를 아래 그래프 처럼 개선시킬 수 있음을 보여줍니다.
Method
SmoothNet은 다음과 같이 나타내며, SmoothNet g는 any pose/body estimatior $f$에서 생성된 noisy estimation $\hat{Y} \in \mathbb{R}^{L \times C} $를 학습하게 됩니다. 여기서 g는 제안하는 SmoothNet이며, $\hat{G} \in \mathbb{R}^{L \times C}$은 refined pose를 뜻합니다.
SmoothNet의 설계는 아래 그림과 같습니다. 꽤 간단한 듯 합니다! 🎉
long-term temporal relationship을 capture하기 위해 natural idea는 유효한 receptive field를 증가시키는 것 입니다. fully-connected layer는 다양한 task에서 long-term dependency를 모델링하는데 널리 사용되었으므로 long-term jitter pattern을 학습하기 위해 이러한 representation을 활용합니다. 또한 movement superposition 원리에 따르면 하나의 움직임은 독립적으로 수행되는 여러 움직임의 중첩으로 볼 수 있기 때문에 이러한 원리에 따라 channel C의 각 축은 독립적으로 처리될 수 있습니다. FC layer를 공간 차원에 구현하는 대신 시간 축을 따라 FC layer를 적용합니다. 여기서 시간 축을 따라 residual connection이 있는 FC layer를 구성합니다.
각 layer의 계산은 아래와 수식과 같이 이루어집니다.
여기서 $w^{l}_{t}$와 $b^l$은 $t_{th}$프레임에서 학습할 수 있는 weight와 bias를 뜻하며, 이는 다른 $i_{th}$축에서 각각 공유됩니다. σ는 non-linear activation 함수이며, LeakyReLU를 사용했습니다. SmoothNet으로 $\hat{Y}$을 처리하기 위해 filter와 유사한 sliding window 방식을 사용합니다. 먼저 size $T$의 chunk를 추출하고, refined results를 얻은 다음 step size $s$의 next chunk로 이동하게 됩니다.
또한 SmoothNet에서의 목표는 주로 acceleration error로 나타내는 jitter pattern을 capture하는 것이며, 이를 모델링하는 것은 간단하다고 합니다. 네트워크에서 movement function 즉, 속도와 acceleration를 명시적으로 모델링합니다. 사전에 physical meaning이 주어지면, 1차 및 2차 motion information을 활용하여 학습 프로세스를 Basic SmoothNet 보다 더 빠르게 수렴하는 것이 가능하다고 합니다.
구체적으로 입력 $\hat{Y}$가 주어지면 아래 식에 따라 각 축에 대한 속도와 acceleration를 계산하게 됩니다.
아래 그림에서 볼 수 있듯 top branch는 noise가 있는 위치 $\hat{Y}$를 수정하기 위한 baseline stream 입니다. 다른 two branche들은 noisy velocity $\hat{V}$와 acceleration $\hat{A}$를 입력합니다. long-term cue를 capture하기 위해 본 논문에서는 속도와 acceleration를 개선하기 위하여 식3도 사용합니다. 서로 다른 motion order의 정보를 집계하기 위해 3개의 brench의 top embedding을 연결하고, linear fusion layer를 수행하여 최종적으로 정제된 pose인 $\hat{G}$를 얻습니다.
Loss Function은 아래와 같습니다. 학습 중 position error와 acceleration error를 모두 최소화 하는 것을 목표로 정의합니다.
여기서 $\hat{G^{''}_{j,t}}$는 예측된 pose인 $\hat{G}_{i,t}$에서 계산된 acceleration이고, $\hat{A}_{i,t}$는 GT acceleration 입니다. 그리고 단순히 final target으로 $L_{pose}$, $L_{acc}$를 추가합니다.
Experiments
이제 실험결과를 살펴보겠습니다. SmoothNet에 의해 refine된 backbone 리스트들입니다. multi frame 방법이 아니더라도 적용할 수 있는 듯 합니다. SmoothNet은 첫번째 레이어와 residual connection이 있는 3개의 cascaded block 및 decoder로서의 역할을 수행하는 마지막 레이어를 포함하여 모두 8개의 레이어로 구성됩니다. 이 레이어들의 매개변수는 0.33M이며, 평균 추론시간은 프레임 당 1e-5초 미만이라고 합니다.
아래 그림은 VIBE, Gaussian 1D Filter, SmoothNet의 성능을 비교해놓은 그림입니다.
아래 테이블은 Human3.6M 데이터세트 기반으로 2D, 3D pose estimator에 SmoothNet을 적용한 결과입니다.
다른 데이터 세트에서 비교한 결과에서도 SOTA를 달성합니다. MPJPE, PA-MPJPE가 조금 줄어들었네요. 수치 상으로는 1~3 point 쯤에 불과해 보일지라도 이정도 성능을 달성하기 까지는 굉장히 어려운 일 이라는 것을 잘 알고 있기 때문에 연구 결과가 굉장히 의미있는 수치라고 볼 수 있겠습니다. 👏🏻👏🏻👏🏻👏🏻👏🏻
확실히 정성적인 결과를 확인하면 어떻게 달라졌는지 확실하게 알 수 있습니다.
Conclusion
결론적으로 기존 pose estimation 또는 body estimation 방법에서 temporal smoothness를 처리할 수 있고, 프레임 당 정밀도를 향상시킬 수 있는, 정말 간단하면서도 효과적인 pose refine network 였습니다. 특히 rare pose 또는 occlued pose에서 발생하는 long-term jitter를 처리할 수 있었습니다. 본 논문에서 말하고 있는 limitation은 post-processing model로서 기존 backbone의 성능을 일관되게 개선시킬 수 있지만, 최종적인 성능은 주어진 backbone에 의해 제한된다고 말하고 있습니다. 당연한 이야기이지만 jitter를 상당부분 개선했다는 것 만으로도 좋은 듯 합니다!
프로젝트 홈페이지에 들어가면 여러가지 동영상들을 확인할 수 있으니, 한번 확인해보시는것을 추천 드립니다. 실제로 웅크린 자세를 했을 때 아래와 같이 스무스 하게 만들어주네요. 아래 캡쳐한 사진 맨 왼쪽을 보시면 GT와는 다르게 포즈가 꼬인 것을 확인할 수 있습니다. 이미지로 이렇게 스쳐 지나가듯 보면 별 이상 없어 보이고, 자세히 보아야 확실히 pose가 꼬인게 보입니다. 동영상으로 보면 pose가 꼬인채로 keypoint들이 왔다갔다 하는 모습을 보실 수 있습니다. 이렇게 인간의 눈에서도 이미지와 동영상으로 결과를 확인할 때 차이가 있는데, 컴퓨터는 오죽할까요 👻
모델 코드도 보면 좋을 것 같아서 첨부해봅니다!
import torch
from torch import Tensor, nn
class SmoothNetResBlock(nn.Module):
"""Residual block module used in SmoothNet.
Args:
in_channels (int): Input channel number.
hidden_channels (int): The hidden feature channel number.
dropout (float): Dropout probability. Default: 0.5
Shape:
Input: (*, in_channels)
Output: (*, in_channels)
"""
def __init__(self, in_channels, hidden_channels, dropout=0.5):
super().__init__()
self.linear1 = nn.Linear(in_channels, hidden_channels)
self.linear2 = nn.Linear(hidden_channels, in_channels)
self.lrelu = nn.LeakyReLU(0.2, inplace=True)
self.dropout = nn.Dropout(p=dropout, inplace=True)
def forward(self, x):
identity = x
x = self.linear1(x)
x = self.dropout(x)
x = self.lrelu(x)
x = self.linear2(x)
x = self.dropout(x)
x = self.lrelu(x)
out = x + identity
return out
class SmoothNet(nn.Module):
"""SmoothNet is a plug-and-play temporal-only network to refine human
poses. It works for 2d/3d/6d pose smoothing.
"SmoothNet: A Plug-and-Play Network for Refining Human Poses in Videos",
arXiv'2021. More details can be found in the `paper
<https://arxiv.org/abs/2112.13715>`__ .
Note:
N: The batch size
T: The temporal length of the pose sequence
C: The total pose dimension (e.g. keypoint_number * keypoint_dim)
Args:
window_size (int): The size of the input window.
output_size (int): The size of the output window.
hidden_size (int): The hidden feature dimension in the encoder,
the decoder and between residual blocks. Default: 512
res_hidden_size (int): The hidden feature dimension inside the
residual blocks. Default: 256
num_blocks (int): The number of residual blocks. Default: 3
dropout (float): Dropout probability. Default: 0.5
Shape:
Input: (N, C, T) the original pose sequence
Output: (N, C, T) the smoothed pose sequence
"""
def __init__(self,
window_size: int,
output_size: int,
hidden_size: int = 512,
res_hidden_size: int = 256,
num_blocks: int = 3,
dropout: float = 0.5):
super().__init__()
self.window_size = window_size
self.output_size = output_size
self.hidden_size = hidden_size
self.res_hidden_size = res_hidden_size
self.num_blocks = num_blocks
self.dropout = dropout
assert output_size <= window_size, (
'The output size should be less than or equal to the window size.',
f' Got output_size=={output_size} and window_size=={window_size}')
# Build encoder layers
self.encoder = nn.Sequential(
nn.Linear(window_size, hidden_size),
nn.LeakyReLU(0.1, inplace=True))
# Build residual blocks
res_blocks = []
for _ in range(num_blocks):
res_blocks.append(
SmoothNetResBlock(
in_channels=hidden_size,
hidden_channels=res_hidden_size,
dropout=dropout))
self.res_blocks = nn.Sequential(*res_blocks)
# Build decoder layers
self.decoder = nn.Linear(hidden_size, output_size)
def forward(self, x: Tensor) -> Tensor:
"""Forward function."""
N, C, T = x.shape
x=x.to(torch.float32)
assert T == self.window_size, (
'Input sequence length must be equal to the window size. ',
f'Got x.shape[2]=={T} and window_size=={self.window_size}')
# Forward layers
x = self.encoder(x)
x = self.res_blocks(x)
x = self.decoder(x) # [N, C, output_size]
return x
Project : https://ailingzeng.site/smoothnet
GitHub : https://github.com/cure-lab/SmoothNet
Paper : https://arxiv.org/abs/2112.13715