Blog › OpenCV und ARKit – eine kurze Anleitung für Techfans

Leonardo Galli Leonardo Galli

OpenCV und ARKit: Eine technische Kurzanleitung für Interessierte.

Spätestens seit iOS 11 sind AR-Anwendungen auf Smartphones ein aktuelles und wichtiges Thema. Mit den neusten Entwicklungen ist es bereits erstaunlich gut möglich, Realitätserweiternde Anwendungen zu programmieren.

Zusammen mit dem ARKit und OpenCV lassen sich Objekte (wie zum Beispiel ein Stormtrooper) als Marker verwenden und anschliessend mit dem ARKit virtuell in das Kamerabild platzieren. Dieses Zusammenspiel zwischen ARKit und OpenCV ist jedoch nur mangelhaft dokumentiert und kann entsprechend bei ersten Versuchen viel Zeit kosten. Um Programmierbegeisterte zu unterstützen, haben wir hier eine kleine Kurzanleitung für das Setup-Verfahren geschrieben: Eine technische Anleitung, um ARKit zusammen mit OpenCV einzusetzen (Stand: Dezember 2017).

Inhalt

 

  1. Installation und Setup
  2. Kommunikation zwischen ARKit in Swift und OpenCV in C++
  3. Konvertieren von View Koordinaten zu Bild Koordinaten
  4. Weitere Hilfe bei Problemen

Installation und Setup

Zuerst erstellst du am besten ein neues XCode Projekt und wählst ARKit + SceneKit als Template. Nun lässt sich das OpenCV Framework am einfachsten mit CocoaPods installieren. Hierzu braucht es lediglich folgende Zeile im Podfile:

pod 'OpenCV', '~> 3.3'

Leider kommt diese Version ohne die OpenCV Contrib-Module, welche noch viele zusätzliche und wichtige Algorithmen enthalten. Zudem hat diese Version auch keine Debugging Symbole, was es schwieriger macht, etwaige Performance Probleme zu beheben. Deshalb empfiehlt es sich, das OpenCV Framework eigenhändig zu kompilieren. Dies geht extrem einfach und schmerzlos. Die untenstehenden Zeilen laden die nötigen Files herunter, um mit dem Kompilieren zu beginnen. (Es wird die derzeit aktuellste Version heruntergeladen, 3.3.1. Es lohnt sich mit «git tag» zu prüfen, ob dies immer noch der Fall ist)

mkdir opencv
cd opencv
git clone https://github.com/opencv/opencv.git
git clone https://github.com/opencv/opencv_contrib.git
cd opencv_contrib
git checkout 3.3.1
cd ..
cd opencv
git checkout 3.3.1

Um das Framework zu kompilieren, braucht es die XCode Command Line Tools. Eine detailerte Anleitung, wie du diese installierst, findest du hier (Dezember 2017). Weiter kann das Build Script, welches von OpenCV geliefert wird, keinen Debug Build erstellen. Falls dies erwünscht ist, führe folgendes aus:

# Immer noch in der opencv directory!
wget https://gist.github.com/galli-leo/a76268a52330c612e4b9932a65e4d9e3/raw/a6bff81b19db44e07303996e156ea702e8b06208/build_framework.py -o platforms/ios/build.py

Mit untenstehenden Zeilen kompilierst du nun OpenCV. Falls die Contrib Module nicht erwünscht sind, kann dieser Parameter einfach weggelassen werden.

# Immer noch in der opencv directory!
mkdir build
python platforms/ios/build_framework.py --contrib ../opencv_contrib build

 

Den Debug Build startest du mit folgenden Zeilen falls erwünscht:

# Immer noch in der opencv directory!
mkdir build_debug
python platforms/ios/build_framework.py --contrib ../opencv_contrib --debug build_debug

 

Wenn das Script fertig ist, solltest du nun im build respektive build_debug Ordner das OpenCV Framework vorfinden. Dieses kannst du einfach in dein XCode Projekt ziehen. Dann muss nur noch das OpenCV Framework bei Linked Frameworks and Libraries unter dem General Tab im Projekt hinzugefügt werden

Kommunikation zwischen ARKit in Swift und OpenCV in C++

Da OpenCV in C++ geschrieben ist, kann es nicht direkt in Swift verwendet werden. Deshalb ist eine Wrapper Klasse, geschrieben in Objective-C++ (ein Mix aus C++ und Objective-C), notwendig. Diese wird am besten über File -> New -> New File -> Cocoa Touch Class erstellt. Hier einfach NSObject als Parent und Objective-C als Sprache auswählen. Nun können hier diverse OpenCV Headers importiert werden:

#import <opencv2/opencv.hpp> //Core OpenCV Klassen
#import <opencv2/imgcodecs/ios.h> //Funktionen um zwischen OpenCV und UIImage zu wechseln
#import <opencv2/xfeatures2d.hpp> //Zusätzliche Feature Detector Klassen in den Contrib Modulen

 

Bevor wir aber OpenCV-Funktionen mit ARKit verwenden können, müssen die ARKit frames zuerst in ein OpenCV verständliches Format umgewandelt werden. ARKit übergibt uns für jedes Frame ein CVPixelBuffer. Dieser muss in ein ​cv::Mat​ umgewandelt werden, damit OpenCV damit rechnen kann. Leider ist das Pixel Format vom CVPixelBuffer vom Typ kCVPixelFormatType_420YpCbCr8BiPlanarFullRange. OpenCV erwartet aber immer ein BGR Bild. Beim YpCbCr Pixel Format besteht nicht jeder Pixel aus Rot, Grün und Blau Werten sondern aus dem Helligkeitswert (Luma, Y) und Zwei Farbwerten (Cb, Cr) welche die Abweichung von Grau angeben (Siehe Bild). ​420​ beschreibt das Chroma Subsampling des Pixel Formats (4:2:0). Es bedeutet, dass für jede 2×2 Pixel Region 4 Y werte bestehen und je ein Cb und Cr wert. ​8​ bedeutet, dass Pro Pixel 8 bits verwendet werden (d.h. die Werte gehen von 0 bis 255). ​BiPlanar​ bedeutet, dass zwei Planes gebraucht werden, um die Werte im RAM zu speichern. Konkret bedeutet dies, dass zuerst alle Y Werte kommen, dann allerdings abwechselnd die Cb und Cr Werte.

Cb und Cr Werte veranschaulicht wenn Y = 0.5

Dieses Format kann also folgendermassen zu BGR umgewandelt werden:

+(cv::Mat)matFromPixelBuffer:(CVPixelBufferRef) buffer{
    cv::Mat mat;
    //Lock the base Address so it doesn't get changed!
    CVPixelBufferLockBaseAddress(buffer, 0);
    //Get the data from the first plane (Y)
    void *address =  CVPixelBufferGetBaseAddressOfPlane(buffer, 0);
    int bufferWidth = (int)CVPixelBufferGetWidthOfPlane(buffer,0);
    int bufferHeight = (int)CVPixelBufferGetHeightOfPlane(buffer, 0);
    int bytePerRow = (int)CVPixelBufferGetBytesPerRowOfPlane(buffer, 0);
    //Get the pixel format
    OSType pixelFormat = CVPixelBufferGetPixelFormatType(buffer);
    
    cv::Mat converted;
    //NOTE: CV_8UC3 means unsigned (0-255) 8 bits per pixel, with 3 channels!
    //Check to see if this is the correct pixel format
    if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
        //We have an ARKIT buffer
        //Get the yPlane (Luma values)
        cv::Mat yPlane = cv::Mat(bufferHeight, bufferWidth, CV_8UC1, address);
        
        //Get cbcrPlane (Chroma values)
        int cbcrWidth = (int)CVPixelBufferGetWidthOfPlane(buffer,1);
        int cbcrHeight = (int)CVPixelBufferGetHeightOfPlane(buffer, 1);
        void *cbcrAddress = CVPixelBufferGetBaseAddressOfPlane(buffer, 1);
        //Since the CbCr Values are alternating we have 2 channels: Cb and Cr. Thus we need to use CV_8UC2 here.
        cv::Mat cbcrPlane = cv::Mat(cbcrHeight, cbcrWidth, CV_8UC2, cbcrAddress);
        
        //Split them apart so we can merge them with the luma values
        std::vector<cv::Mat> cbcrPlanes;
        cv::split(cbcrPlane, cbcrPlanes);
        
        cv::Mat cbPlane;
        cv::Mat crPlane;
        
        //Since we have a 4:2:0 format, cb and cr values are only present for each 2x2 luma pixels. Thus we need to enlargen them (by a factor of 2).
        cv::resize(cbcrPlanes[0], cbPlane, yPlane.size(), 0, 0, cv::INTER_NEAREST);
        cv::resize(cbcrPlanes[1], crPlane, yPlane.size(), 0, 0, cv::INTER_NEAREST);
        
        cv::Mat ycbcr;
        std::vector<cv::Mat> allPlanes = {yPlane, cbPlane, crPlane};
        cv::merge(allPlanes, ycbcr);
        
        //ycbcr now contains all three planes. We need to convert it from YCbCr to RGB so OpenCV can work with it
        
        cv::cvtColor(ycbcr, converted, cv::COLOR_YCrCb2RGB);
    } else {
        //Probably RGB so just use that.
        converted = cv::Mat(bufferHeight, bufferWidth, CV_8UC3, address, bytePerRow).clone();
    }

    //Since we clone the cv::Mat no need to keep the Buffer Locked while we work on it.
    CVPixelBufferUnlockBaseAddress(buffer, 0);
    
    return converted;
}

 

Die obenstehende Funktion kann nun verwendet werden, um alle OpenCV Funktionen mit ARKit zu verwenden. Um Beispielsweise Objekte (wie einen Stormtrooper oder einen Marker) wiederzuerkennen, kann die Tracker Klasse von hier verwendet werden:

-(NSArray*)boundingBoxForFrame:(CVPixelBufferRef) buffer {
    cv::Mat mat = [OpenCVWrapper matFromPixelBuffer:buffer];
    cv::Mat gray;
    //Keypoint detection likes GRAY images.
    cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);
    tracker->process(gray);
    vector<Point2f> bb = tracker->current_bb;
    if (bb.size() > 3) {
        return @[[NSValue valueWithCGPoint:CGPointMake((CGFloat)bb[0].x, (CGFloat)bb[0].y)], [NSValue valueWithCGPoint:CGPointMake((CGFloat)bb[1].x, (CGFloat)bb[1].y)], [NSValue valueWithCGPoint:CGPointMake((CGFloat)bb[2].x, (CGFloat)bb[2].y)], [NSValue valueWithCGPoint:CGPointMake((CGFloat)bb[3].x, (CGFloat)bb[3].y)]];
    } else {
        return [[NSArray alloc] init];
    }
}

 

In Swift kann diese Funktion nun folgendermassen gebraucht werden. Es ist extrem wichtig, dass diese Funktion jedoch nur aufgerufen wird, wenn der letzte Aufruf der Funktion fertig ist. Sonst kann der gebrauchte Zwischenspeicher nicht richtig freigegeben werden und es kommt zu einem Memory Leak!

func session(_ session: ARSession, didUpdate frame: ARFrame) {
  // Prevent memory leak
  if self.isPredicting {
    return
  }
  self.isPredicting = true
  if let bb_points = self.wrapper.boundingBox(forFrame: frame.capturedImage) {
    self.isPredicting = false
    var bbPoints = [CGPoint]()
    for p in bb_points {
        if let bbp = p as? CGPoint {
            bbPoints.append(bbp)
        }
    }
    self.drawPoints(points: bbPoints)
  } else {
      self.drawPoints(points: [])
  }
}

 

Konvertieren von View Koordinaten zu Bildkoordinaten

Die Koordinaten, welche von der Tracking-Klasse kommen, sind jedoch in Relation zu dem Bild berechnet. Diese müssen zu Koordinaten in Relation zum View konvertiert werden. ARKit erschwert dies vehement, da es Aspect Fill braucht. Aspect Fill bedeutet, dass das Bild so weit vergrössert wird, bis es den ganzen Hintergrund ausfüllt. Somit werden garantiert Teile des Bildes abgeschnitten. Um herauszufinden um wie viel das Bild vergrössert (oder auch verkleinert) wurde, kann folgende Funktion verwendet werden:

typealias AspectFillInfo = (scale : CGFloat, scaledW : CGFloat, scaledH : CGFloat)

/// Calculates the right scale factors for view and image size
///
func calculateAspectFillInfo(viewSize : CGSize, imageSize : CGSize) -> AspectFillInfo {
    var scaledW : CGFloat = 1.0
    var scaledH : CGFloat = 1.0
    
    let iw = imageSize.width
    let ih = imageSize.height
    let w = viewSize.width
    let h = viewSize.height
    
    var scale : CGFloat = 1.0
    
    //Was imaged scaled until height was full or until width was full?
    if (isHeightSame(viewSize: viewSize, imageSize: imageSize)) {
        scale = h / ih
    } else {
        scale = w / iw
    }
    
    scaledH = scale * ih
    scaledW = scale * iw
    
    return (scale : scale, scaledW : scaledW, scaledH : scaledH)
}

/// Finds out whether width or height will be cutoff when Aspect Fill is chosen.
///
func isHeightSame(viewSize: CGSize, imageSize: CGSize) -> Bool {
    let viewAR = viewSize.width / viewSize.height
    let imageAR = imageSize.width / imageSize.height
    
    return viewAR < imageAR
}

Variablen mit i sind in Relation zum Bild, variablen mit s oder scaled in Relation zum vergrösserten Bild und variablen ohne Präfix in Relation zum View. Dank diesen Infos können nun die Koordinaten eines beliebigen Bildpunktes \(P(ix,iy)\) zum richtigen Punkt im View \(P(x,y)\) konvertiert werden. Diese liegen nun genau über dem Bild im Hintergrund des Views. Zuerst wird also der Punkt im skalierten Bild berechnet (\(P(x,y)\)):

\(
sx = ix * scale \\
sy = iy * scale \\
\)

Da durch Aspect Fill Teile des Bildes abgeschnitten werden, muss diese „überflüssige“ Breite oder Höhe abgezogen werden:

\(
x = sx – \frac{scaledW – w}{2} \\
y = sy – \frac{scaledH – h}{2} \\
\)

Um von View Koordinaten auf Bild Koordinaten müssen lediglich die Formeln von Oben invertiert werden:

\(
sx = x + \frac{scaledW – w}{2} \\
sy = y + \frac{scaledH – h}{2} \\
ix = \frac{sx}{scale} \\
iy = \frac{sx}{scale} \\
\)

Von diesem Punkt an sollte die Verwendung von OpenCV zusammen mit dem ARKit für die meisten Anwendungen (vergleichsweise) einfach sein.

Zu kompliziert oder doch noch Fragen?

Natürlich hoffen wir, dass dir dieser kleine Einstieg ein wenig geholfen hat. Es mag allerdings sicherlich auf manch eine Person auch kompliziert wirken. Ist man jedoch in diesen Bereichen bereits ein wenig geübt, so funktionieren solche Anwendungen bereits in relativ kurzer Zeit. Die Cubera Solutions AG entwickelt seit einigen Jahren mit viel Erfahrung und Expertise AR-Produkte direkt für ihre Kunden. Suchst du also noch einen passenden Projektpartner für dein digitales Problem, so können wir dir nur empfehlen, dich bei uns zu melden.

Weitere Informationen

Weitere (AR-)Projekte

Zum Showcase

Kontakt

Kontakt