Tworzenie natywnych procesorów klatek dla Vision Camera w React Native z użyciem OpenCV

Zmiana architektury w React Native na Fabric, a w jej skutek zastosowanie synchronicznej komunikacji między częścią natywną a JS, umożliwiło stworzenie bibliotek, które prowadzą do świetnej wydajności, nieosiągalnej wcześniej.

Jedną z bibliotek umożliwiającą taką komunikację jest react-native-reanimated, którą wykorzystam wspólnie z react-native-vision-camera do stworzenia aplikacji umożliwiającej wykrywanie elementów z kamery urządzenia w czasie rzeczywistym z wykorzystaniem biblioteki OpenCV.

OpenCV_logo

OpenCV to wieloplatformowa i otwarta biblioteka służąca do obróbki obrazu, stworzona w języku C++, z możliwością skorzystania z nakładek dla innych języków takich jak Java, JavaScript, Python.

Obecnie brak jest jednak sensownych bibliotek dla React Native, które umożliwiły by w sposób prosty wykorzystanie funkcjonalności OpenCV bezpośrednio w kodzie JS. Z pomocą przychodzi nam jednak możliwość wykorzystania kodu natywnego oraz stworzenie komunikacji między wątkiem natywnym, a wątkiem JS.

Standardowe podejście opisywane w niektórych postach, umożliwia stworzenie mostu, z którym komunikacja będzie przebiegać w sposób asynchroniczny. Jednak jest to podejście, które często nie działa wystarczająco sprawnie w taki sposób, aby dokonywać detekcji czy przekształceń w czasie rzeczywistym.

Istotna informacja
Wpis był tworzony z wykorzystaniem OpenCV 4.6.0 oraz React Native 0.68.2. W przypadku innych wersji biblioteki, niektóre kroki mogą się różnić. Zwłaszcza importowania OpenCV do projektu.

Tworzenie projektu

Programowanie - codeblock
Głównym problemem przy wykorzystywaniu OpenCV w aplikacjach React Native jest konieczność tworzenia kodu natywnego.

Pierwszym krokiem będzie stworzenie nowej aplikacji z wykorzystaniem polecenia: 

npx react-native init opencvframeprocessor

Po instalacji niezbędnych podów oraz stworzeniu katalogów, przechodzimy do importowania OpenCV do naszego projektu, oddzielnie dla systemu iOS i Android.

Importowanie OpenCV

Importowanie OpenCV
OpenCV – wielopatformowa biblioteka funkcji wykorzystywanych podczas obróbki obrazu, oparta na otwartym kodzie i zapoczątkowana przez Intela. Jej autorzy skupiają się na przetwarzaniu obrazu w czasie rzeczywistym.
iOS

Importowanie OpenCV dla iOS

Pierwszym krokiem będzie pobranie OpenCV w wersji dla iOS. W moim przypadku jest to wersja 4.6.0.
OpenCV w wersji dla iOS
OpenCV w wersji dla iOS.

Po pobraniu biblioteki, uruchamiamy nasz projekt w Xcode (pamiętajmy aby był to projekt z rozszerzeniem .xcworkspace. Aby zaimportować bibliotekę, przeciągamy pobrany katalog o nazwie opencv2.framework do głównego projektu (lewy panel okna).

Importowanie OpenCV w Xcode
Importowanie OpenCV w Xcode.

Następnie zaznaczamy opcję >Copy items if needed< oraz klikamy >Finish<. Biblioteka powinna pojawić się w panelu po lewej stronie okna.

Kolejnym krokiem, będzie dołączenie do projektu wymaganych frameworków. Możemy to zrobić w ustawieniach projektu -> Build Phases -> Link Binary With Libraries.

Do projektu powinna zostać dodana następująca lista elementów:

  • QuartzCore.framework,
  • CoreVideo.framework,
  • CoreImage.framework,
  • AssetsLibrary.framework,
  • CoreFoundation.framework,
  • CoreGraphics.framework,
  • CoreMedia.framework,
  • Accelerate.framework.
Importowanie frameworków dla OpenCV
Importowanie frameworków dla OpenCV.

Następnym krokiem będzie stworzenie plików z obsługą OpenCV w projekcie. Pliki tworzymy w katalogu głównym projektu (tam gdzie znajdują się pliki AppDelegate.h i AppDelegate.m). 

Najpierw tworzymy nowy plik z headerem naszego pliku – nazwijmy go OpenCV.h.

Header file w Xcode
Header File w Xcode.

Deklarujemy w nim nową klasę oraz przykładową metodę do pobierania wersji OpenCV. Aby to zrobić korzystamy z poniższego kodu.

#ifndef OpenCV_h
#define OpenCV_h

#include <Foundation/Foundation.h>

@interface OpenCV: NSObject
+ (NSString *) getOpenCVVersion;
@end

#endif /* OpenCV_h */

Następnie tworzymy nowy plik Objective-C o nazwie OpenCV.m.

Objective-C File w Xcode
Objective-C File w Xcode.

Jako, że biblioteka OpenCV jest napisana z wykorzystaniem C++, konieczna będzie zmiana formatu utworzonego pliku na format .mm (czyli inaczej Objective C++). Możemy to zrobić w prawym panelu (poprzez dopisanie formatu do nazwy pliku).

Objective Cpp w Xcode
Objective Cpp w Xcode.

Następnie w utworzonym pliku tworzymy kod implementujący klasę z pliku z nagłówkiem. 

#import <Foundation/Foundation.h>
#import "OpenCV.h"
#import <opencv2/opencv.hpp>

@implementation OpenCV : NSObject

+ (NSString *) getOpenCVVersion {
  return [NSString stringWithFormat:@"Version: %s", CV_VERSION];
}

@end

Kolejnym krokiem będzie utworzenie pliku PCH, w którym dodamy informację, że biblioteka OpenCV będzie wymagała kompilatora dla języka Objective C++. Aby to zrobić dodajemy nowy plik PCH o nazwie PrefixHeader w lokalizacji pozostałych, wcześniej utworzonych plików.

PCH File w Xcode
PCH File w Xcode.

I ustawiamy mu następującą treść:

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#ifdef __cplusplus
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#endif

#endif

Następnie w ustawieniach projektu musimy wskazać jego lokalizację.
W tym celu w Build Settings -> Prefix Header dodajemy wpis o treści: ${PROJECT_DIR}/PrefixHeader.pch.

Lokalizacja pliku PCH
Lokalizacja pliku PCH.

Po tym wszystkim sprawdzamy czy aplikacja się buduje – jeżeli tak, nasza biblioteka została dodana poprawnie i możemy przejść do kolejnych kroków.

Android

Importowanie OpenCV dla Androida

Aby pobrać bibliotekę OpenCV dla Androida. Wracamy do strony głównej OpenCV, z której pobieraliśmy bibliotekę dla iOS jednak tym razem wybieramy pakiet dla systemu Android.

OpenCV w wersji dla Androida
OpenCV w wersji dla Androida.

Po pobraniu i rozpakowaniu archiwum otwieramy nasz projekt w Android Studio. Pierwszym krokiem do importu naszego modułu będzie wybranie opcji File -> Import module oraz wskazaniu lokalizacji do katalogu sdk (Uwaga! nie będzie to katalog sdk/java). Bibliotekę nazywamy, np. openCVLib, a pozostałe opcje pozostawiamy domyślnie.

Importowanie modułu w Android Studio
Importowanie modułu w Android Studio.
Importowanie modułu w Android Studio - ścieżka do pliku
Importowanie modułu w Android Studio - ścieżka do pliku.

Następnie musimy dodać wsparcie dla języka Kotlin. W pliku build.gradle dodajemy następujące elementy:

ext {
    ...
    kotlin_version = '1.6.10'
    ...
}

...

dependencies {
    ...
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}

Przechodzimy do dodania biblioteki jako zależności dla projektu. W menu File wybieramy opcję Project Structure.

Project Structure w Android Studio
Project Structure w Android Studio.

Wchodzimy w zakładkę Dependencies i klikamy ikonę + wybierając opcję Module Dependency. W kolejnym kroku wybieramy naszą bibliotekę OpenCV i dodajemy zależność.

Module dependency w Android Studio
Module dependency w Android Studio.

Kolejnym krokiem będzie dodanie plików jniLibs do naszej aplikacji. W katalogu app/src/main tworzymy katalog jniLibs i kopiujemy tam zawartość katalogu sdk/native/libs z wcześniej pobranego archiwum.

jniLibs w Android Studio
jniLibs w Android Studio.

W pliku   dodajemy następującą linijkę, aby naprawić błąd przy budowaniu aplikacji.

android {
    // ...
    packagingOptions {
        pickFirst '**/*.so'
    }
}

Następnie musimy sprawdzić czy biblioteka została poprawnie zaimportowana. W pliku MainActivity.java importujemy pakiet biblioteki.

import org.opencv.android.OpenCVLoader;

Oraz w klasie MainActivity dodajemy statyczne pole:

static {
  if(OpenCVLoader.initDebug()) {
    Log.d(„TEST”, "opencv loaded");
  }
}

Po zbudowaniu i włączeniu aplikacji w logach powinien wyświetlić się komunikat o załadowaniu OpenCV.

Instalacja wymaganych bibliotek

Instalacja niezbędnych bibliotek dla naszego projektu.
Biblioteka VisionCamera pozwala tworzyć aplikacja do obsługi kamer. Między innymi daje nam kontrolę nad tym, jakie urządzenie jest używane oraz możliwość konfiguracji wielu opcji.

Jak wspomniałem wcześniej, kolejnym krokiem będzie dodanie bibliotek Vision Camera oraz Reanimated do naszego projektu. W tym celu w katalogu głównym projektu React Native wykonujemy polecenia:

yarn add react-native-vision-camera react-native-reanimated
npx pod-install
Aby poprawnie dodać bibliotekę oraz dodać niezbędne uprawnienia, przejdź przez proces instalacji opisany tutaj: https://mrousavy.com/react-native-vision-camera/docs/guides.

Tworzenie Frame Processorów

Frame Processor
Frame Processor to funkcja napisana w JavaScript (lub TypeScript), która może być używana do przetwarzania klatek, które "widzi" kamera.
iOS

Frame Processor dla iOS

Aby stworzyć nowy frame procesor dla biblioteki Vision Camera, konieczne jest stworzenie pliku w którym zaszyjemy logikę. Jednak zanim to zrobimy musimy rozbudować nasz plik OpenCV.mm o funkcje umożliwiające detekcję obiektów.

W naszym przypadku będzie to wykrywanie niebieskiego kwadratu. Frame processor domyślnie zwraca nam klatkę z aparatu w formie obiektu o typie CMSampleBufferRef i dlatego konieczne będzie przygotowanie funkcji, która umożliwi nam jego konwersję na standardowy obraz używany w iOS o typie UIImage. Możemy to zrobić za pomocą funkcji (dodajmy ją w klasie OpenCV w pliku OpenCV.mm):

+ (UIImage *) toUIImage:(CMSampleBufferRef)samImageBuff
  {
       CVImageBufferRef imageBuffer =
         CMSampleBufferGetImageBuffer(samImageBuff);
       CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer];
       CIContext *temporaryContext = [CIContext contextWithOptions:nil];
       CGImageRef videoImage = [temporaryContext
                         createCGImage:ciImage
                         fromRect:CGRectMake(0, 0,
                         CVPixelBufferGetWidth(imageBuffer),
                         CVPixelBufferGetHeight(imageBuffer))];

       UIImage *image = [[UIImage alloc] initWithCGImage:videoImage];
       CGImageRelease(videoImage);
      return image;
}

Bibioteka OpenCV wykonuje operacje na tzw. matrycach. Stąd konieczna będzie funkcja, która umożliwi nam konwersję UIImage na obiekt typu Mat. Możemy to zrobić na przykład w następujący sposób:

+ (cv::Mat)cvMatFromUIImage:(UIImage *)image
{
  CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
  CGFloat cols = image.size.width;
  CGFloat rows = image.size.height;
  cv::Mat cvMat(rows, cols, CV_8UC4);
  CGContextRef contextRef = CGBitmapContextCreate(cvMat.data,
                                                 cols,
                                                 rows,
                                                 8,
                                                 cvMat.step[0],
                                                 colorSpace,
                                                 kCGImageAlphaNoneSkipLast |
                                                 kCGBitmapByteOrderDefault);
  CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
  CGContextRelease(contextRef);
  return cvMat;
}

Następnie dodajmy funkcję w której będzie implementowane wykrywanie niebieskich obiektów. 

+ (NSDictionary *)findObjects:(UIImage *)image {
}

Nasza detekcja będzie przebiegała w następujący sposób:

  • Obrazek domyślnie zapisany w formacie RGB, przekonwertujemy na BGR oraz następnie na HSV.
  • Bazując na przedziale wytniemy tylko taki kolor który nas interesuje (będzie to niebieski).
  • Wykryjemy kontury niebieskich elementów.
  • Pierwszy z nich większy niż sprecyzowana wartość będzie naszym wykrytym elementem, więc zwrócimy jego pozycję oraz wielkość.

Na początku sprecyzujmy więc nasze przedziały wartości. Dla koloru niebieskiego, będą to np.

 cv::Vec3b lowerBound(90, 120, 120);
  cv::Vec3b upperBound(140, 255, 255);

Następnie wykonajmy niezbędne transformacje kolorów.

cv::Mat matBGR, hsv;
std::vector<cv::Mat> channels;

cv::Mat matRGB = [self cvMatFromUIImage:(image)];
cv::cvtColor(matRGB,matBGR,cv::COLOR_RGB2BGR);
cv::cvtColor(matBGR,hsv,cv::COLOR_BGR2HSV);
cv::inRange(hsv, lowerBound, upperBound, hsv);
cv::split(hsv, channels);

Następnie wykryjmy nasz kontur, i zwróćmy w postaci obiektu NSDictionary (tak aby był możliwy do odebrania po stronie JS).

std::vector<std::vector<cv::Point>> contours;
cv::findContours(channels[0], contours, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE );

std::vector<NSDictionary *> rects;

for( int i = 0; i< contours.size(); i++ ) {
    double area = contourArea(contours[i],false);
    if (area>3000) {
        cv::Rect rect = cv::boundingRect(contours.at(i));
      
        return @{@"x": [NSNumber numberWithInt:rect.x] , @"y":
                            [NSNumber numberWithInt: rect.y], @"width": [NSNumber numberWithInt:rect.width], @"height": [NSNumber numberWithInt:rect.height] };
      }
  }

W przypadku braku takich elementów w klatce, zwróćmy pusty obiekt.

return @{};

Dodane przez nas funkcje musimy dodać do pliku z nagłówkiem tj. OpenCV.h. Po zmianach będzie on wyglądał następująco.

#ifndef OpenCV_h
#define OpenCV_h

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

@interface OpenCV: NSObject
+ (NSString *) getOpenCVVersion;
+ (UIImage *) toUIImage:(CMSampleBufferRef)samImageBuff;
+ (NSDictionary *)findObjects:(UIImage *)image;
@end

#endif /* OpenCV_h */

Następnie musimy utworzyć nowy plik z naszym Frame procesorem. Nazwijmy go ObjectDetectFrameProcessor.mm i dodajmy do niego następujący kod.

#import <Foundation/Foundation.h>
#import <VisionCamera/FrameProcessorPlugin.h>
#import <VisionCamera/Frame.h>
#import <opencv2/opencv.hpp>
#import "OpenCV.h"

@interface ObjectDetectFrameProcessor : NSObject
@end

@implementation ObjectDetectFrameProcessor

static inline id objectDetect(Frame* frame, NSArray* args) {
  CMSampleBufferRef buffer = frame.buffer;
  return [OpenCV findObjects:[OpenCV toUIImage:buffer]];
}

VISION_EXPORT_FRAME_PROCESSOR(objectDetect)

@end

Tak dodany kod, możemy już wykorzystać w kodzie JS. Najpierw jednak dodajmy podobną funkcjonalność także dla systemu Android.

Android

Frame Processor dla Androida

Domyślnym formatem zwracanym przez bibliotekę Vision Camera w Frame procesorze dla Androida jest ImageProxy. Aby dodać wsparcie dla niego w pliku app/build.gradle w sekcji dependencies musimy dodać:

implementation 'androidx.camera:camera-core:1.1.0-beta02'

Dodajmy plik OpenCV.java, który będzie zawierał funkcję findObject, która będzie odpowiadała za wykrywanie niebieskich obiektów. Dodatkowo dodajmy pomocnicza metodę do konwersji obiektu ImageProxy na obiekt Mat.

package com.opencvframeprocessor;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.YuvImage;

import com.facebook.react.bridge.WritableNativeMap;

import org.opencv.android.Utils;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import androidx.camera.core.ImageProxy;

public class OpenCV {
    static WritableNativeMap findObjects(Mat matRGB) {
        Scalar lowerBound = new Scalar(90, 120, 120);
        Scalar upperBound = new Scalar(140, 255, 255);

        Mat matBGR = new Mat(), hsv = new Mat();
        List<Mat> channels = new ArrayList<>();

        Imgproc.cvtColor(matRGB, matBGR, Imgproc.COLOR_RGB2BGR);
        Imgproc.cvtColor(matBGR, hsv, Imgproc.COLOR_BGR2HSV);
        Core.inRange(hsv, lowerBound, upperBound, hsv);
        Core.split(hsv, channels);

        List<MatOfPoint> contours = new ArrayList<>();
        Mat hierarchy = new Mat();

        Imgproc.findContours(channels.get(0), contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE);

        for (int i = 0; i < contours.size(); i++) {
            MatOfPoint contour = contours.get(i);
            double area = Imgproc.contourArea(contour);

            if(area > 3000) {
                Rect rect = Imgproc.boundingRect(contour);
                WritableNativeMap result = new WritableNativeMap();
                result.putInt("x", rect.x);
                result.putInt("y", rect.y);
                result.putInt("width", rect.width);
                result.putInt("height", rect.height);

                return result;
            }
        }

      return new WritableNativeMap();
    }

    static Mat imageToMat(ImageProxy imageProxy) {
        ImageProxy.PlaneProxy[] plane = imageProxy.getPlanes();
        ByteBuffer yBuffer = plane[0].getBuffer();
        ByteBuffer uBuffer = plane[1].getBuffer();
        ByteBuffer vBuffer = plane[2].getBuffer();

        int ySize = yBuffer.remaining();
        int uSize = uBuffer.remaining();
        int vSize = vBuffer.remaining();

        byte[] nv21 = new byte[ySize + uSize + vSize];

        yBuffer.get(nv21, 0, ySize);
        vBuffer.get(nv21, ySize, vSize);
        uBuffer.get(nv21, ySize + vSize, uSize);
        try {
            YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, imageProxy.getWidth(), imageProxy.getHeight(), null);
            ByteArrayOutputStream stream = new ByteArrayOutputStream(nv21.length);
            yuvImage.compressToJpeg(new android.graphics.Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 90, stream);
            Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
            Matrix matrix = new Matrix();
            matrix.postRotate(90);
            stream.close();
            Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
            Mat mat = new Mat(rotatedBitmap.getWidth(), rotatedBitmap.getHeight(), CvType.CV_8UC4);
            Utils.bitmapToMat(rotatedBitmap, mat);
            return mat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Aby dodać Frame Processor, musimy stworzyć plik ObjectDetectFrameProcessorPlugin.java z następującą zawartością:

package com.opencvframeprocessor;

import androidx.annotation.NonNull;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin;

import java.util.Collections;
import java.util.List;

import javax.annotation.Nonnull;

public class ObjectDetectFrameProcessorPluginModule implements ReactPackage {
    @NonNull
    @Override
    public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
        FrameProcessorPlugin.register(new ObjectDetectFrameProcessorPlugin());
        return Collections.emptyList();
    }

    @Nonnull
    @Override
    public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

Oraz ObjectDetecFrameProcessorPluginModule.java:

package com.opencvframeprocessor;

import androidx.camera.core.ImageProxy;
import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin;

import org.opencv.core.Mat;

public class ObjectDetectFrameProcessorPlugin extends FrameProcessorPlugin {
    @Override
    public Object callback(ImageProxy image, Object[] params) {
        Mat mat = OpenCV.imageToMat(image);
        return OpenCV.findObjects(mat);
    }

    ObjectDetectFrameProcessorPlugin() {
        super("objectDetect");
    }
}

Następnie moduł musi zostać zarejestrowany. W pliku MainApplication.java pod linijką:

List<ReactPackage> packages = new PackageList(this).getPackages();

Dodajemy nowy wpis z naszym modułem.

packages.add(new ObjectDetectFrameProcessorPluginModule());

Tak przygotowany moduł jest już gotowy do użycia w kodzie JS. 

Javascript

Wykorzystanie Frame Procesorów po stronie JS

Java Script logo

Aby umożliwić korzystanie z procesora klatek po stronie aplikacji, musimy dodać możliwość wykrywania go przez plugin react-native-reanimated. Aby to zrobić musimy dodać odpowiedni wpis w pliku babel.config.js (znajduje się on w katalogu głównym aplikacji).

plugins: [
    [
      'react-native-reanimated/plugin',
      {
        globals: ['__objectDetect'],
      },
    ],
  ],

Nazwa __objectDetect nie jest przypadkowa – taką samą podaliśmy w kodzie natywnym naszych procesorów. Dodajemy tylko znaki „__” na początku nazwy.

Przejdźmy do pliku App.js. Najpierw musimy zadeklarować naszą funkcję odpowiedzialną za wywołanie kodu natywnego. 

function objectDetect(frame) {
  'worklet';
  return __objectDetect(frame);
}

W komponencie App dodajemy następnie nasz kod. Najpierw zacznijmy od zadeklarowania miejsca do przechowywania parametrów wykrytego kwadratu.

const flag = useSharedValue({height: 0, left: 0, top: 0, width: 0});

const flagOverlayStyle = useAnimatedStyle(
    () => ({
      backgroundColor: 'blue',
      position: 'absolute',
      ...flag.value,
    }),
    [flag],
);

Dzięki zastosowaniu hooka useSharedValue możemy przekazywać wartości pozycji i wielkości kwadratu bezpośrednio do stylu wykorzystującego hook useAnimatedStyle. Oba pochodzą z biblioteki react-native-reanimated.

Ważną sprawą jest również sprawdzenie uprawnień do aparatu, bez tego nie uda nam się uruchomić aparatu.

useEffect(() => {
    const checkPermissions = async () => {
      await Camera.requestCameraPermission();
    };
    checkPermissions();
  }, []);

Przejdźmy do deklaracji frame procesora. Po wykryciu obiektu, musimy przekonwertować wartość pozycji i wielkości z klatki z aparatu na wielkości rozdzielczości ekranu urządzenia (z powodu, że mają one różne wymiary). Z powodu, że wielkość klatki jest podawana odwrotnie na iOS niż na Android musimy dokonać zamiany wielkości.

const dimensions = useWindowDimensions();

const frameProcessor = useFrameProcessor(frame => {
    'worklet';
    const rectangle = objectDetect(frame);
    
    const xFactor =
      dimensions.width / Platform.OS === 'ios' ? frame.width : frame.height;
    const yFactor =
      dimensions.height / Platform.OS === 'ios' ? frame.height : frame.width;
    
    if (rectangle.x) {
      flag.value = {
        height: rectangle.height * yFactor,
        left: rectangle.x * xFactor,
        top: rectangle.y * yFactor,
        width: rectangle.width * xFactor,
      };
    } else {
      flag.value = {height: 0, left: 0, top: 0, width: 0};
    }
}, []);

Następnie nasz komponent musi zwrócić komponent <Camera /> oraz animowany kwadrat.

if (device == null) {
    return null;
  }

  return (
    <>
      <Camera
        frameProcessor={frameProcessor}
        style={StyleSheet.absoluteFill}
        device={device}
        isActive={true}
        orientation="portrait"
      />
      <Animated.View style={flagOverlayStyle} />
    </>
  );

Cały plik wygląda następująco:

import React, {useEffect} from 'react';
import 'react-native-reanimated';
import {Platform, StyleSheet, useWindowDimensions} from 'react-native';
import {
  Camera,
  useFrameProcessor,
  useCameraDevices,
} from 'react-native-vision-camera';
import {useSharedValue, useAnimatedStyle} from 'react-native-reanimated';
import Animated from 'react-native-reanimated';

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

function App() {
  const flag = useSharedValue({height: 0, left: 0, top: 0, width: 0});

  const flagOverlayStyle = useAnimatedStyle(
    () => ({
      backgroundColor: 'blue',
      position: 'absolute',
      ...flag.value,
    }),
    [flag],
  );

  const dimensions = useWindowDimensions();

  const frameProcessor = useFrameProcessor(frame => {
    'worklet';
    const rectangle = objectDetect(frame);

    const xFactor =
      dimensions.width / Platform.OS === 'ios' ? frame.width : frame.height;
    const yFactor =
      dimensions.height / Platform.OS === 'ios' ? frame.height : frame.width;

    if (rectangle.x) {
      flag.value = {
        height: rectangle.height * yFactor,
        left: rectangle.x * xFactor,
        top: rectangle.y * yFactor,
        width: rectangle.width * xFactor,
      };
    } else {
      flag.value = {height: 0, left: 0, top: 0, width: 0};
    }
  }, []);

  const devices = useCameraDevices();
  const device = devices.back;

  useEffect(() => {
    const checkPermissions = async () => {
      await Camera.requestCameraPermission();
    };
    checkPermissions();
  }, []);

  if (device == null) {
    return null;
  }

  return (
    <>
      <Camera
        frameProcessor={frameProcessor}
        style={StyleSheet.absoluteFill}
        device={device}
        isActive={true}
        orientation="portrait"
      />
      <Animated.View style={flagOverlayStyle} />
    </>
  );
}

export default App;

Rezultaty

Sprawdźmy jak działa nasz kod na obu systemach.

iOS

Podgląd działania aplikacji dla systemu iOS

Rezultat działania aplikacji na systemie iOS.
Rezultat działania aplikacji na systemie iOS.
Android

Podgląd działania aplikacji dla systemu Android

Rezultaty działania aplikacji na systemie Android
Rezultaty działania aplikacji na systemie Android.

Podsumowanie

Proces importowania biblioteki OpenCV oraz wykorzystanie jej do detekcji obiektów w czasie rzeczywistym nie jest łatwym zadaniem. Mnogość wersji oraz sposób ich wykorzystania, często dostarcza wielu problemów trudnych do rozwiązania. 

Nie mniej jednak efekt jest wystarczającą nagrodą za przebytą drogę. Niestety głównym problemem przy wykorzystywaniu OpenCV w aplikacjach React Native jest konieczność tworzenia kodu Natywnego czy to w Java (lub Kotlin) w przypadku systemu Android, czy Objective C/C++ (lub Swift) w przypadku iOS. 

  1. https://brainhub.eu/library/opencv-react-native-image-processing – bardzo dobry artykuł zawierający sposób na importowanie OpenCV do React Native. Niestety nie jest aktualny dla najnowszych wersji biblioteki.
  2. https://opencv.org – strona główna biblioteki OpenCV, zawierająca dokumentację oraz przykłady użycia.
  3. https://mrousavy.com/react-native-vision-camera/docs/guides/frame-processors – dokumentacja zawierająca przykłady użycia procesorów klatek.
Łukasz Kurant

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

Zostaw komentarz

15 + dwa =

Top