Detekcja pozy w czasie rzeczywistym w React Native z użyciem MLKit

Detekcja pozycji człowieka na podstawie filmów lub obrazów ma kluczową rolę w wielu nowoczesnych zastosowaniach. Określanie poprawności wykonywania ćwiczeń fizycznych, nakładanie filtrów w rzeczywistości rozszerzonej (filtry w wielu aplikacjach społecznościowych), ale również rozpoznawanie języka migowego czy zastosowania medyczne – w tych wszystkich sytuacjach istnieje potrzeba sprawnego modelu rozpoznawania pozycji ciała człowieka.

W 2020 roku naukowcy Valentin Bazarevsky oraz Ivan Grishchenko z firmy Google zaprezentowali światu narzędzie BlazePose które na stałe weszło do MLKit od Google, i służy właśnie do wykrywania pozycji z pojedynczej klatki filmu, zapewniając przy tym obsługę oraz przetwarzanie w czasie rzeczywistym.

Jeśli interesuje Was ten wpis zachęcamy do zapoznania się również z pozostałymi artykułami o wykorzystaniu sztucznej inteligencji i uczenia maszynowego w aplikacjach React Native:

1. Tworzenie natywnych procesorów klatek dla Vision Camera w React Native z użyciem OpenCV.
2. Analiza semantyczna w React Native z wykorzystaniem Tensorflow.

Topologia

W przeciwieństwie do obecnego standardu w przetwarzaniu pozy ludzkiego działa, czyli topologii COCO, składającej się z 17 punktów orientacyjnych, BlazePose posiada możliwość umieszczenia aż 33 punktów, zarówno na kończynach człowieka (używając modelu dłoni) jak i samej twarzy.

Szczegółowy zbiór punktów możemy zobaczyć poniżej:

Topologia punktów obserwacyjnych
Topologia punktów obserwacyjnych (źródło: https://ai.googleblog.com/2020/08/on-device-real-time-body-pose-tracking.html)

Sposób działania

Wykrywanie pozy ma charakter dwuskładnikowy: najpierw detektor lokalizuje tzw. obszar zainteresowania (ROI), w tym przypadku będzie to człowiek zlokalizowany na zdjęciu. Następnie  przywidywane są punkty orientacyjne. Dla przyspieszenia obliczeń, pierwsza część jest wykonywana wyłącznie na pierwszej klatce – do obliczeń kolejnych, wykorzystywane są punkty z poprzedniej.

Detektor pozy
Detektor pozy (źródło: https://ai.googleblog.com/2020/08/on-device-real-time-body-pose-tracking.html)

Przykład użycia

W tym artykule chciałbym zaprezentować przykład użycia MLKit do detekcji pozy w czasie rzeczywistym w aplikacji React Native z wykorzystaniem biblioteki Vision Camera z wykorzystaniem natywnego procesora klatek dla iOS.

Krok 1

Konfiguracja projektu

Konfiguracja projektu
Pierwszym krokiem będzie stworzenie nowego projektu aplikacji React Native.

Wykorzystywana przeze mnie wersja to React Native 0.68.2. Aby stworzyć nowy projekt wykonujemy polecenie:

npx react-native init posedetection

Musimy również zainstalować potrzebne biblioteki do obsługi kamery oraz animacji:

yarn add react-native-vision-camera react-native-reanimated react-native-svg
npx pod-install

Koniecznym krokiem w przypadku systemu iOS jest dodanie wpisu w pliku Info.plist:

<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) needs access to your Camera.</string>

Aby zainstalować bibliotekę umożliwiająca detekcję pozy, wykorzystując pakiety CocoaPods, w pliku Podfile dodajemy następujący wpis:

pod 'GoogleMLKit/PoseDetection', '3.1.0'

I następnie wykonujemy polecenie:

npx pod-install
Krok 2

Tworzenie procesora klatek

Procesor klatek
Aby umożliwić wykorzystanie biblioteki MLKit w czasie rzeczywistym w bibliotece Vision Camera, konieczne jest stworzenie natywnego procesora klatek.

Aby to wykonać w głównym katalogu projektu w Xcode stwórzmy nowy plik PoseDetection.h z nagłówkiem naszej klasy zwracającej rozpoznany obiekt.

#ifndef PoseDetection_h
#define PoseDetection_h

#include <Foundation/Foundation.h>
#import <UIKit/UIImage.h>
#import <CoreMedia/CMSampleBuffer.h>
#import <VisionCamera/Frame.h>

@interface PoseDetection: NSObject
+ (NSDictionary *)findPose:(Frame *)frame;
@end

#endif /* PoseDetection_h */

Następnie musimy stworzyć plik PoseDetection.m, gdzie będzie znajdowała się nasza funkcja findPose.

#import <Foundation/Foundation.h>
#import "PoseDetection.h"

@implementation PoseDetection : NSObject

+ (NSDictionary *)findPose:(Frame *)frame {
  
}
@end

Utwórzmy również funkcję pomocniczą zwracającą koordynaty wybranego punktu (jednego z wcześniej opisanych, 33 punktów orientacyjnych):

+ (NSDictionary *)getLandmarkPosition:(MLKPoseLandmark *)landmark {
  MLKVision3DPoint *position = landmark.position;
  return @{
    @"x": [NSNumber numberWithDouble:position.x],
    @"y": [NSNumber numberWithDouble:position.y]
  };
}

Następnie w funkcji findPose przygotujmy obraz klatki oraz obliczmy pozycję naszego obiektu:

CMSampleBufferRef buffer = frame.buffer;
UIImageOrientation orientation = frame.orientation;

MLKPoseDetectorOptions *options = [[MLKPoseDetectorOptions alloc] init];
  options.detectorMode = MLKPoseDetectorModeStream;
  
MLKPoseDetector *poseDetector =
      [MLKPoseDetector poseDetectorWithOptions:options];

MLKVisionImage *image = [[MLKVisionImage alloc] initWithBuffer:buffer];
image.orientation = orientation;

NSError *error;
NSArray *poses = [poseDetector resultsInImage:image error:&error];

W przypadku, gdy funkcja wykrywająca zwróci błąd oraz w przypadku, gdy nie zostanie wykryta żadna poza, zwróćmy pusty obiekt typu NSDictionary. W przypadku wykrycia pozy, zwróćmy wybrane koordynaty:

if (error != nil) {
    // Error.
    return @{};
}
  
if (poses.count == 0) {
    // No pose detected.
    return @{};
}
  
for (MLKPose *pose in poses) {
    return @{
      @"leftShoulder": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeLeftShoulder]],
      @"rightShoulder": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeRightShoulder]],
      @"leftElbow": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeLeftElbow]],
      @"rightElbow": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeRightElbow]],
      @"leftWrist": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeLeftWrist]],
      @"rightWrist": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeRightWrist]],
      @"leftHip": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeLeftHip]],
      @"rightHip": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeRightHip]],
      @"leftKnee": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeLeftKnee]],
      @"rightKnee": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeRightKnee]],
      @"leftAnkle": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeLeftAnkle]],
      @"rightAnkle": [self getLandmarkPosition:[pose landmarkOfType:MLKPoseLandmarkTypeRightAnkle]],
    };
}

Kolejnym krokiem będzie stworzenie pliku PoseDetectionFrameProcessor.m, który będzie bezpośrednio wykorzystany przez bibliotekę Vision Camera:

#import <Foundation/Foundation.h>
#import <VisionCamera/FrameProcessorPlugin.h>
#import <VisionCamera/Frame.h>
#import "PoseDetection.h"

@interface PoseDetectionFrameProcessor : NSObject
@end

@implementation PoseDetectionFrameProcessor

static inline id poseDetection(Frame* frame, NSArray* args) {
  CMSampleBufferRef buffer = frame.buffer;
  UIImageOrientation orientation = frame.orientation;
  
  return [PoseDetection findPose:frame];
}

VISION_EXPORT_FRAME_PROCESSOR(poseDetection)

@end

Nasz procesor klatek będzie nosił nazwę poseDetection oraz będzie zwracał obiekt typu NSDictionary (który będzie konwertowany do zwykłego obiektu po stronie JavaScriptu).

Krok 3

Obsługa po stronie JavaScript

Aby umożliwić wykorzystanie procesora klatek, w pliku babel.config.js musimy dodać następujący element:
plugins: [
    [
      'react-native-reanimated/plugin',
      {
        globals: ['__poseDetection'],
      },
    ],
],

Gdzie __poseDetection jest nazwą procesora klatek, poprzedzoną dwoma znakami „_”.

Następnie w pliku App.js dodajmy funkcję umożliwiającą jego wykorzystanie:

export function objectDetect(frame) {
  'worklet';
  return __poseDetection(frame);
}

Aby trzymać obliczone pozycje punktów orientacyjnych użyjmy hooka useSharedValue z biblioteki react-native-reanimated:

const defaultPose = {
  leftShoulder: {x: 0, y: 0},
  rightShoulder: {x: 0, y: 0},
  leftElbow: {x: 0, y: 0},
  rightElbow: {x: 0, y: 0},
  leftWrist: {x: 0, y: 0},
  rightWrist: {x: 0, y: 0},
  leftHip: {x: 0, y: 0},
  rightHip: {x: 0, y: 0},
  leftKnee: {x: 0, y: 0},
  rightKnee: {x: 0, y: 0},
  leftAnkle: {x: 0, y: 0},
  rightAnkle: {x: 0, y: 0},
};

const pose = useSharedValue(defaultPose);

Następnie musimy obliczyć koordynaty linii pomiędzy punktami orientacyjnymi:

const leftWristToElbowPosition = usePosition(pose, 'leftWrist', 'leftElbow');
const leftElbowToShoulderPosition = usePosition(pose, 'leftElbow', 'leftShoulder');
const leftShoulderToHipPosition = usePosition(pose, 'leftShoulder', 'leftHip');
const leftHipToKneePosition = usePosition(pose, 'leftHip', 'leftKnee');
const leftKneeToAnklePosition = usePosition(pose, 'leftKnee', 'leftAnkle');

const rightWristToElbowPosition = usePosition(pose, 'rightWrist', 'rightElbow');
const rightElbowToShoulderPosition = usePosition(pose, 'rightElbow', 'rightShoulder');
const rightShoulderToHipPosition = usePosition(pose, 'rightShoulder', 'rightHip');
const rightHipToKneePosition = usePosition(pose, 'rightHip', 'rightKnee');
const rightKneeToAnklePosition = usePosition(pose, 'rightKnee', 'rightAnkle');

const shoulderToShoulderPosition = usePosition(pose, 'leftShoulder', 'rightShoulder');
const hipToHipPosition = usePosition(pose, 'leftHip', 'rightHip');

usePosition to hook umożliwiający stworzenie stylu wykorzystywanego przez bibliotekę reanimated:

const usePosition = (pose, valueName1, valueName2) => {
  return useAnimatedStyle(
    () => ({
      x1: pose.value[valueName1].x,
      y1: pose.value[valueName1].y,
      x2: pose.value[valueName2].x,
      y2: pose.value[valueName2].y,
    }),
    [pose],
  );
};

Dzięki temu, możemy je później użyć do wyświetlenia linii na ekranie. Przejdźmy najpierw jednak do samego obliczenia potrzebnych kordynatów. Poniższy kod ma za zadanie wykorzystać natywny procesor do obliczenia pozycji punktów orientacyjnych oraz wykorzystując proporcje z ekranu użytkownika (tzw. xFactor i yFactor) do zapisu pozycji punktów na ekranie użytkownika. 

const dimensions = useWindowDimensions();

const frameProcessor = useFrameProcessor(frame => {
    'worklet';
    const poseObject = objectDetect(frame);
    
    const xFactor = dimensions.width / frame.width;
    const yFactor = dimensions.height / frame.height;
    
    const poseCopy = {
      leftShoulder: {x: 0, y: 0},
      rightShoulder: {x: 0, y: 0},
      leftElbow: {x: 0, y: 0},
      rightElbow: {x: 0, y: 0},
      leftWrist: {x: 0, y: 0},
      rightWrist: {x: 0, y: 0},
      leftHip: {x: 0, y: 0},
      rightHip: {x: 0, y: 0},
      leftKnee: {x: 0, y: 0},
      rightKnee: {x: 0, y: 0},
      leftAnkle: {x: 0, y: 0},
      rightAnkle: {x: 0, y: 0},
    };
    
    Object.keys(poseObject).forEach(v => {
      poseCopy[v] = {
        x: poseObject[v].x * xFactor,
        y: poseObject[v].y * yFactor,
      };
    });

    pose.value = poseCopy;
}, []);

W funkcji return naszego komponentu App zwracamy komponent <Camera /> wykorzystujący nasz frameProcessor:

<Camera
    frameProcessor={frameProcessor}
    style={StyleSheet.absoluteFill}
    device={device}
    isActive={true}
    orientation="portrait"
    frameProcessorFps={15}
  />

Aby narysować animowane linie z wykorzystaniem biblioteki react-native-reanimated użyjmy do tego komponentów z react-native-svg:

const AnimatedLine = Animated.createAnimatedComponent(Line);

//...

<Svg
    height={Dimensions.get('window').height}
    width={Dimensions.get('window').width}
    style={styles.linesContainer}>
    <AnimatedLine animatedProps={leftWristToElbowPosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={leftElbowToShoulderPosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={leftShoulderToHipPosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={leftHipToKneePosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={leftKneeToAnklePosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={rightWristToElbowPosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={rightElbowToShoulderPosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={rightShoulderToHipPosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={rightHipToKneePosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={rightKneeToAnklePosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={shoulderToShoulderPosition} stroke="red" strokeWidth="2" />
    <AnimatedLine animatedProps={hipToHipPosition} stroke="red" strokeWidth="2" />
</Svg>
Krok 4

Efekty

Po wykonaniu wszystkich kroków, sprawdźmy jak działa nasza aplikacja:

Podsumowanie

Wykorzystywanie wykrywania pozycji człowieka, otwiera niezwykłe możliwości przy tworzeniu wieloplatformowych aplikacji mobilnych, a możliwość skorzystania z gotowych narzędzi jak MLKit, to znaczne ułatwienie.   

Nowa architektura fabric oraz biblioteki jak Vision Camera czy Reanimated pozwalają na tworzenie szybkiej komunikacji między kodem natywnym, a kodem JavaScript, co w konsekwencji prowadzi do wielu nowych, ciekawych zastosowań i znaczącej optymalizacji działania aplikacji.

Link do repozytorium z kodem wykorzystanym w artykule:
https://github.com/dogtronic/blog-pose-detection

Link do wpisu w języku angielskim:
Real-time pose detection in React Native using MLKit

  • https://ai.googleblog.com/2020/08/on-device-real-time-body-pose-tracking.html
  • https://google.github.io/mediapipe/solutions/pose
  • https://developers.google.com/ml-kit/vision/pose-detection/ios
  • https://mrousavy.com/react-native-vision-camera/docs/guides/frame-processors-plugins-ios
Łukasz Kurant

Programista aplikacji mobilnych i webowych z kilkuletnim stażem. Entuzjasta React Native oraz rozwiązań multi-platformowych.

Zostaw komentarz

dziewiętnaście − 16 =

Top