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.