본문 바로가기

AI

BlazePalm 모델 정리

MediaPipe 모델은 hand pose estimation에서 우수한 실시간 성능으로 많이 쓰이는 모델 중 하나이다.

 

논문을 간단히 정리하자면,

 

하나의 RGB 카메라로 손바닥을 통한 손 bounding box 생성 → 손의 골격 예측

간단한 구조로 모바일 GPU에서도 실시간 추론 속도🙆‍♀️

 

아키텍쳐는

  1. 손 bounding box를 통해 손바닥을 찾는 손바닥 검출기
    장점 : 데이터 증강의 필요성 줄고 랜드마크의 좌표를 찾는 곳에 집중 & 모든 프레임에서 bounding box 찾을 필요 없음(이전 프레임의 랜드마크 예측을 이용하기 때문)
  2. 1.의 결과를 통해 2.5D 랜드마크를 반환하는 손 랜드마크 모델

로 구성되어 있다.

이때 손바닥 검출기로 이용되는 모델이 바로 BlazePalm 모델이다. 


BlazePalm 개요

BlazePalm 모델도 BlazeHand 모델 중 한 부분이라고 할 수 있는데,

간단히 정리하자면 BlazeHand = BlazePalm + BlazeLandmark 이다.

앞서 말했던 mediapipe 모델의 아키텍처와 유사한 것이... BlazeHand가 mediapipe의 핵심임을 알 수 있다!

 

BlazeHand 모델은 BlazeFace모델과 아키텍처가 유사하며, 간단하게 BlazeFace를 손에 적용한 모델이다.

 

따라서 BlazeHand에 대해 알아보기 전에 BalzeFace 모델의 아키텍처에 대해 간단하게 알아보자.


BlazeFace 아키텍처

BlazeFace는 MobileNetV1, V2에서 영감을 받아 변형하였다는 점을 기억하면 이해하는 데에 도움이 될 것 같다.

 

1. Enlarging the receptive field sizes

깊이별 합성곱의 커널 크기를 늘리는 것이 상대적으로 비용이 적게 듦. → bottleneck에서 5x5 커널 ⇒ 특정 수용 영역에 도달하기 위해 필요한 bottleneck 지점의 총 수를 줄임

MobileNetV2의 bottleneck 구조 : 확장(깊이 증가) + 투영(깊이 감소)을 비선형 활성화 함수로 분리한 구조 → 중간 텐서 수가 적어지도록 재구성하여 bottleneck 지점의 잔여 연결(residual connection)이 확장된 채널 해상도에서 작동하도록 조정

깊이별 합성곱 낮은 비용 활용하여 두 점별 합성곱(?) (pointwise convolutions) 사이에 추가로 깊이별 합성곱 계층을 삽입 ⇒ 수용 영역 확장 가속화, “이중 BlazeBlock”의 핵심 형성

 

2. Feature extractor

전면 카메라 모델을 위한 특징 추출기에 초점 → 더 작은 객체 크기 범위 처리해야

⇒ 계산 요구사항 낮음

 

입력 : 128x128 RGB

 

5개의 단일 BlazeBlock, 6개의 이중 BlazeBlock으로 구성

 

 

 

 

 

 

 

 

 

3. Anchor scheme

Anchor : 객체 탐지 모델이 탐지할 수 있는 사전 정의된 경계 상자. 모델은 이를 기준으로 객체의 위치와 크기를 조정

각 앵커에 대해 중심 오프셋, 크기 조정과 같은 회귀 매개변수가 예측 → 매개변수는 사전 정의된 앵커 위치를 조정하여 bounding box로 변환하는 데 사용

 

다운 샘플링 → 계산 자원 최적화 전형적인 SSD 모델은 1×1, 2×2, 4×4, 8×8, 16×16 크기의 특징 맵에서 예측을 수행 하지만 특정 특징 맵 해상도에 도달한 이후 추가 계산이 불필요함(PPN(Pooling Pyramid Network)) ⇒ 8x8 특징 맵 해상도에서 다운샘플링을 중단

 

8×8, 4×4, 2×2 해상도 각각에서 픽셀당 2개의 앵커를 8×8 해상도에서 6개의 앵커로 대체 인간 얼굴의 종횡비 변화가 제한적이기 때문에, 1:1 종횡비로 앵커를 제한

 

4. Post-processing

특징 추출기 8x8 이하로 줄이지 않기 때문에 특정 객체와 겹치는 앵커의 수 객체 크기 커질수록 크게 증가

기존 비최대 억제(non-maximum suppression, NMS) 시나리오에서 여러 앵커 중 하나만 선택되어 최종 알고리즘 결과로 사용

→ 비디오에서는 프레임 사이에 앵커 달라져 예측값 떨림(jitter) 발생

⇒ 블렌딩 전략 : 중첩된 예측값 사이의 회귀 매개변수를 가중 평균으로 추정


Blaze Palm 코드 구현

참고 링크

https://github.com/zmurez/MediaPipePyTorch/tree/master

 

GitHub - zmurez/MediaPipePyTorch: Port of MediaPipe tflite models to PyTorch

Port of MediaPipe tflite models to PyTorch. Contribute to zmurez/MediaPipePyTorch development by creating an account on GitHub.

github.com

 

코드와 함께 간단한 설명을 정리했다.

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

from blazebase import BlazeDetector, BlazeBlock

class BlazePalm(BlazeDetector):
    """The palm detection model from MediaPipe. """
    def __init__(self):
        super(BlazePalm, self).__init__()

        # These are the settings from the MediaPipe example graph
        # mediapipe/graphs/hand_tracking/subgraphs/hand_detection_gpu.pbtxt
        
        # mediapipe 예제 그래프 설정을 사용하여 클래스 변수를 초기화
        self.num_classes = 1
        self.num_anchors = 2944
        self.num_coords = 18
        self.score_clipping_thresh = 100.0
        self.x_scale = 256.0
        self.y_scale = 256.0
        self.h_scale = 256.0
        self.w_scale = 256.0
        self.min_score_thresh = 0.5
        self.min_suppression_threshold = 0.3
        self.num_keypoints = 7

        # These settings are for converting detections to ROIs which can then
        # be extracted and feed into the landmark network
        # use mediapipe/calculators/util/detections_to_rects_calculator.cc
        self.detection2roi_method = 'box'
        # mediapipe/graphs/hand_tracking/subgraphs/hand_detection_cpu.pbtxt
        
        # 감지된 손가락을 ROI로 변환하기 위한 설정
        self.kp1 = 0
        self.kp2 = 2
        self.theta0 = np.pi/2
        self.dscale = 2.6
        self.dy = -0.5

	# 모델의 레이어를 정의
        self._define_layers()

 

    def _define_layers(self):
        self.backbone1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=2, padding=0, bias=True),
            nn.ReLU(inplace=True),

            BlazeBlock(32, 32),
            BlazeBlock(32, 32),
            BlazeBlock(32, 32),
            BlazeBlock(32, 32),
            BlazeBlock(32, 32),
            BlazeBlock(32, 32),
            BlazeBlock(32, 32),
            
            BlazeBlock(32, 64, stride=2),
            BlazeBlock(64, 64),
            BlazeBlock(64, 64),
            BlazeBlock(64, 64),
            BlazeBlock(64, 64),
            BlazeBlock(64, 64),
            BlazeBlock(64, 64),
            BlazeBlock(64, 64),

            BlazeBlock(64, 128, stride=2),
            BlazeBlock(128, 128),
            BlazeBlock(128, 128),
            BlazeBlock(128, 128),
            BlazeBlock(128, 128),
            BlazeBlock(128, 128),
            BlazeBlock(128, 128),
            BlazeBlock(128, 128),

        )

먼저 backbone1부터 보자.

첫번째 Conv2d 에서 3개의 채널을 가진 RGB 컬러이미지 입력을 32 채널로 변환한다.

stride = 2로 설정하여 다운샘플링을 통해 공간 해상도를 줄이면

(batch_size, 128, H/8, W/8)크기가 출력된다.

 

        self.backbone2 = nn.Sequential(
            BlazeBlock(128, 256, stride=2),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
        )

backbone2

채널 수 : 128 -> 256

backbone과 마찬가지로 stride = 2를 통해 다운샘플링 

(batch_size, 256, H/16, W/16) 크기 출력

        self.backbone3 = nn.Sequential(
            BlazeBlock(256, 256, stride=2),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
            BlazeBlock(256, 256),
        )

backbone3

채널 수 유지

마찬가지로 stride = 2 통한 다운샘플링

(batch_size, 256, H/32, W/32) 크기 출력

 

        self.conv_transpose1 = nn.ConvTranspose2d(in_channels=256, out_channels=256, kernel_size=2, stride=2, padding=0, bias=True)
        # 입력 : backbone3의 출력(batch_size, 256, H/32, W/32)
        # 출력 : (batch_size, 256, H/16, W/16) = backbone2 출력 크기
        self.blaze1 = BlazeBlock(256, 256)

        self.conv_transpose2 = nn.ConvTranspose2d(in_channels=256, out_channels=128, kernel_size=2, stride=2, padding=0, bias=True)
        # 입력 : backbone2의 출력(batch_size, 256, H/16, W/16)
        # 출력 : (batch_size, 128, H/8, W/8) = backbone1 출력 크기
        self.blaze2 = BlazeBlock(128, 128)

conv_transpose1, convtranspose2에서 업샘플링을 실시한다.

이를 통해 낮은 해상도의 특징을 상위 해상도로 복원하고, 높은 해상도의 특징과 통합한다.(통합하는 코드는 밑에서 등장)

 

그렇다면 업샘플링은 왜 필요한걸까?

업샘플링이란 저해상도 feature map을 더 높은 해상도로 복원하기 위한 단계로,네트워크가 더 많은 공간 정보를 포함한 고해상도 feature map을 생성하고 다른 해상도의 정보와 통합하여 다중 스케일에서 학습할 수 있도록 하기 위해!

설명을 들어도 잘 이해가 되지 않았는데,

https://dacon.io/en/forum/406022

 

여러가지 Upsampling 방식들

 

dacon.io

 

이 링크 속 설명을 참고하면 nn.ConvTranspose()에 대한 부분도 충분히 이해할 수 있을 것 같다.

 

	# 클래스 예측
        self.classifier_32 = nn.Conv2d(128, 2, 1, bias=True)
        self.classifier_16 = nn.Conv2d(256, 2, 1, bias=True)
        self.classifier_8 = nn.Conv2d(256, 6, 1, bias=True)

        # 회귀 예측
        self.regressor_32 = nn.Conv2d(128, 36, 1, bias=True)
        self.regressor_16 = nn.Conv2d(256, 36, 1, bias=True)
        self.regressor_8 = nn.Conv2d(256, 108, 1, bias=True)

 

   def forward(self, x):
        b = x.shape[0]      # batch size, needed for reshaping later

        x = F.pad(x, (0, 1, 0, 1), "constant", 0)
        # 입력 텐서 가장자리 0으로 패딩
        # (오른쪽, 아래쪽에 각각 1픽셀씩 패딩 추가

        # 백본 처리
        x = self.backbone1(x)           # (b, 128, 32, 32)        
        y = self.backbone2(x)           # (b, 256, 16, 16)
        z = self.backbone3(y)           # (b, 256, 8, 8)

        # feature map 정보 연결하기 위해 업샘플링 사용 -> 다중 스케일 정보가 통합
        y = y + F.relu(self.conv_transpose1(z), True)
        y = self.blaze1(y)

        x = x + F.relu(self.conv_transpose2(y), True)
        x = self.blaze2(x)
	# 클래스 예측
    	c1 = self.classifier_8(z)       # (b, 2, 16, 16)
        c1 = c1.permute(0, 2, 3, 1)     # (b, 16, 16, 2)
        c1 = c1.reshape(b, -1, 1)       # (b, 512, 1)

        c2 = self.classifier_16(y)      # (b, 6, 8, 8)
        c2 = c2.permute(0, 2, 3, 1)     # (b, 8, 8, 6)
        c2 = c2.reshape(b, -1, 1)       # (b, 384, 1)

        c3 = self.classifier_32(x)      # (b, 6, 8, 8)
        c3 = c3.permute(0, 2, 3, 1)     # (b, 8, 8, 6)
        c3 = c3.reshape(b, -1, 1)       # (b, 384, 1)

        c = torch.cat((c3, c2, c1), dim=1)  # (b, 896, 1)

        # 회귀 예측
        r1 = self.regressor_8(z)        # (b, 32, 16, 16)
        r1 = r1.permute(0, 2, 3, 1)     # (b, 16, 16, 32)
        r1 = r1.reshape(b, -1, 18)      # (b, 512, 16)

        r2 = self.regressor_16(y)       # (b, 96, 8, 8)
        r2 = r2.permute(0, 2, 3, 1)     # (b, 8, 8, 96)
        r2 = r2.reshape(b, -1, 18)      # (b, 384, 16)

        r3 = self.regressor_32(x)       # (b, 96, 8, 8)
        r3 = r3.permute(0, 2, 3, 1)     # (b, 8, 8, 96)
        r3 = r3.reshape(b, -1, 18)      # (b, 384, 16)

        r = torch.cat((r3, r2, r1), dim=1)  # (b, 896, 16)

        return [r, c]

이 과정들(permute, reshape)이 필요한 이유는 PyTorch와 TensorFlow Lite의 텐서 형식이 다르기 때문이다

(PyTorch의 텐서는 NCHW 형식 (배치, 채널, 높이, 너비)을 사용하지만 TensorFlow Lite(TFLite)는 NHWC 형식 (배치, 높이, 너비, 채널))

따라서 TFLite로 내보내기 전에 텐서의 차원을 변환이 필요한 것이다.

 

각 단계를 자세히 살펴보면,

permute : pytorch 텐서 형식(NCHW) -> Tensorflow Lite 텐서 형식(NHWC)로 변환

reshape : 클래스 예측값이나 회귀값을 단일 벡터로 변환

torch.cat : 여러 단계에서 생성된 결과를 하나의 텐서로 병합

 

참고 링크

https://arxiv.org/pdf/1907.05047

https://arxiv.org/pdf/2006.10214

'AI' 카테고리의 다른 글

HOG | HONV 정리  (0) 2025.01.09
영상 처리 기초 정리  (0) 2025.01.02
[논문 리뷰] Neural Feedback Text Clustering with BiLSTM-CNN-Kmeans  (3) 2024.12.05
Video Classification  (2) 2024.11.21
[추천시스템] 행렬 분해 정리  (0) 2024.11.15