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.


