Einfache iOS App mit Live Capture Object Detection von Verkehrsschildern

Mithilfe von CreateML und Xcode wurde ein Model und eine App erstellt, die es ermöglichen, Verkehrszeichen zu erkennen.

Dataset

Als Datenbasis wurde eine Mischung aus dem German Traffic Sign Benchmark Dataset sowie selber aufgenommenen Bildern genutzt.

Die Bilder wurden im Folgenden mit cloud.annotations.ai von IBM manuell annotiert und folglich exportiert. So wurden über 400 Bilder in Farbe zum trainieren des Algorithmus genutzt.

Die App

Zur Anwendung des Algorithmus, dessen Entwicklung in der Folge beschrieben wird, wurde eine einfache iOS App mit Swift entwickelt, welche Objekte erkennen kann und sie mithilfe von Bounding Boxes markieren kann. Der Vorteil der App ist, dass es durch CoreML einfach gemacht wird, neue Modelle zur Object Detection einzufügen oder auszutauschen. Im Folgenden ist der Code für den View Controller zu finden:

//
//  ViewController.swift
//  SpeedWatch
//
//  Created by Leon Sick on 04.03.20.
//  Copyright © 2020 Leon Sick. All rights reserved.
//

import UIKit
import AVKit
import Vision

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
    
    let detectionOverlay = CALayer()
    
    let identifierLabel: UILabel = {
        let label = UILabel()
        label.backgroundColor = .white
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let captureSession = AVCaptureSession()
        captureSession.sessionPreset = .vga640x480
        
        guard let captureDevice = AVCaptureDevice.default(for: .video) 
else { return }
        guard let input = try? AVCaptureDeviceInput(device: captureDevice) else { return }
        captureSession.addInput(input)
        
        captureSession.startRunning()
        
        let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
        let rootLayer = view.layer
        previewLayer.frame = rootLayer.bounds
        rootLayer.addSublayer(previewLayer)
        detectionOverlay.name = "DetectionOverlay"
        detectionOverlay.bounds = CGRect(x: 0.0,
                                         y: 0.0,
                                         width: self.view.frame.width,
                                         height: self.view.frame.height)
        detectionOverlay.position = CGPoint(x: rootLayer.bounds.midX, y: rootLayer.bounds.midY)
        rootLayer.addSublayer(detectionOverlay)
        
        let videoDataOutput = AVCaptureVideoDataOutput()
        captureSession.addOutput(videoDataOutput)
        
        videoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
        
        captureSession.commitConfiguration()
        
//        VNImageRequestHandler(cgImage: <#T##CGImage#>, options: [:]).perform(<#T##requests: [VNRequest]##[VNRequest]#>)
        
        setupIdentifierConfidenceLabel()
    }
    
    fileprivate func setupIdentifierConfidenceLabel() {
        view.addSubview(identifierLabel)
        identifierLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -32).isActive = true
        identifierLabel.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        identifierLabel.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        identifierLabel.heightAnchor.constraint(equalToConstant: 50).isActive = true
    }
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
//        print("Camera was able to capture a frame:", Date())
        
        guard let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        
        guard let model = try? VNCoreMLModel(for: FirstTrafficSignDetector_v2().model) else { return }
        
        let objectRecognition = VNCoreMLRequest(model: model, completionHandler: { (request, error) in
            
            
            DispatchQueue.main.async(execute: {
                // perform all the UI updates on the main queue
                if let results = request.results {
                    self.drawVisionRequestResults(results)
                }
            })
        })
        try? VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]).perform([objectRecognition])
    }
    
    
    
    

    func drawVisionRequestResults(_ results: [Any])
    {
        CATransaction.begin()
        CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
        detectionOverlay.sublayers = nil // remove all the old recognized objects
        for observation in results where observation is VNRecognizedObjectObservation {
            guard let objectObservation = observation as? VNRecognizedObjectObservation else {
                continue
            }
            // Select only the label with the highest confidence.
            let topLabelObservation = objectObservation.labels[0]
            let pct = Float(Int(topLabelObservation.confidence * 10000)) / 100
            print(topLabelObservation.identifier.uppercased())
            self.identifierLabel.text = "\(topLabelObservation.identifier.uppercased()), \(pct)%"
            let objectBounds = VNImageRectForNormalizedRect(objectObservation.boundingBox, Int(self.view.frame.width), Int(self.view.frame.height))

            var shapeLayer = CALayer()
            var textLayer = CALayer()

            shapeLayer = self.createRoundedRectLayerWithBounds(objectBounds)

//            textLayer = self.createTextSubLayerInBounds(objectBounds,
//                                                            identifier: topLabelObservation.identifier,
//                                                            confidence: topLabelObservation.confidence)
            shapeLayer.addSublayer(textLayer)
            self.detectionOverlay.addSublayer(shapeLayer)
        }
        self.updateLayerGeometry()
        CATransaction.commit()
    }
    
    func createTextSubLayerInBounds(_ bounds: CGRect, identifier: String, confidence: VNConfidence) -> CATextLayer {
           let textLayer = CATextLayer()
           textLayer.name = "Object Label"
           let formattedString = NSMutableAttributedString(string: String(format: "\(identifier)\nConfidence:  %.2f", confidence))
           let largeFont = UIFont(name: "Helvetica", size: 24.0)!
           formattedString.addAttributes([NSAttributedString.Key.font: largeFont], range: NSRange(location: 0, length: identifier.count))
           textLayer.string = formattedString
           textLayer.bounds = CGRect(x: 0, y: 0, width: bounds.size.height - 10, height: bounds.size.width - 10)
           textLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
           textLayer.shadowOpacity = 0.7
           textLayer.shadowOffset = CGSize(width: 2, height: 2)
           textLayer.foregroundColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.0, 0.0, 0.0, 1.0])
           textLayer.contentsScale = 2.0 // retina rendering
           // rotate the layer into screen orientation and scale and mirror
           textLayer.setAffineTransform(CGAffineTransform(rotationAngle: CGFloat(.pi / 2.0)).scaledBy(x: 1.0, y: -1.0))
           return textLayer
       }
       
       func createRoundedRectLayerWithBounds(_ bounds: CGRect) -> CALayer {
           let shapeLayer = CALayer()
           shapeLayer.bounds = bounds
           shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
           shapeLayer.name = "Found Object"
           shapeLayer.backgroundColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1.0, 1.0, 0.2, 0.4])
           shapeLayer.cornerRadius = 7
           return shapeLayer
       }
        
        func updateLayerGeometry() {
            let bounds = view.layer.bounds
            var scale: CGFloat
            
            let xScale: CGFloat = bounds.size.width / self.view.frame.height
            let yScale: CGFloat = bounds.size.height / self.view.frame.width
            
            scale = fmax(xScale, yScale)
            if scale.isInfinite {
                scale = 1.0
            }
            
            CATransaction.begin()
            CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
            
            // rotate the layer into screen orientation and scale and mirror
            detectionOverlay.setAffineTransform(CGAffineTransform(rotationAngle: CGFloat(.pi / 2.0)).scaledBy(x: scale, y: -scale))
            // center the layer
            detectionOverlay.position = CGPoint (x: bounds.midX, y: bounds.midY)
            
            CATransaction.commit()
            
        }
        
  

In der Umsetzung auf dem iPhone besitzt die App dann folgende Anzeige zur Erkennung der Verkehrsschilder.

Version 1 des Object Detection Algorithmus

Für die erste Version des Algorithmus wurde ein Tool von Apple namens CreateML genutzt. In dem Tool wurde ein Object Detection Algorithmus auf die gelabelten Bilder trainiert über 3300 Iterationen. Das aktuellste Ergebnis ist mit einer Validation Rate von 19% noch immer ernüchternd, eine erweiterte Datenbasis sollte das Problem jedoch lösen. Hier ein Screenshot des Tools von Apple:

Nach Fertigstellung des Algorithmus muss in der App nur eine Zeile Code geändert werden, um das Model zu erneuern bzw. auszutauschen.

guard let model = try? VNCoreMLModel(for: FirstTrafficSignDetector_v2().model) else { return }

Erlangte Kenntnisse für Version 1

  • Swift Basics
  • CreateML
  • Annotationen für Computer Vision Projekte

Version 2 des Object Detection Algorithmus

Für Version 2 des Algorithmus wird der TensorFlow Object Recognition API benutzt werden. Sobald ein Ergebnis vorliegt, wird es hier hochgeladen.