Spaces:
Sleeping
Sleeping
// Copyright 2019 The TensorFlow Authors. All Rights Reserved. | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
import AVFoundation | |
import UIKit | |
import os | |
public struct PixelData { | |
var a: UInt8 | |
var r: UInt8 | |
var g: UInt8 | |
var b: UInt8 | |
} | |
extension UIImage { | |
convenience init?(pixels: [PixelData], width: Int, height: Int) { | |
guard width > 0 && height > 0, pixels.count == width * height else { return nil } | |
var data = pixels | |
guard let providerRef = CGDataProvider(data: Data(bytes: &data, count: data.count * MemoryLayout<PixelData>.size) as CFData) | |
else { return nil } | |
guard let cgim = CGImage( | |
width: width, | |
height: height, | |
bitsPerComponent: 8, | |
bitsPerPixel: 32, | |
bytesPerRow: width * MemoryLayout<PixelData>.size, | |
space: CGColorSpaceCreateDeviceRGB(), | |
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue), | |
provider: providerRef, | |
decode: nil, | |
shouldInterpolate: false, | |
intent: .defaultIntent) | |
else { return nil } | |
self.init(cgImage: cgim) | |
} | |
} | |
class ViewController: UIViewController { | |
// MARK: Storyboards Connections | |
@IBOutlet weak var previewView: PreviewView! | |
//@IBOutlet weak var overlayView: OverlayView! | |
@IBOutlet weak var overlayView: UIImageView! | |
private var imageView : UIImageView = UIImageView(frame:CGRect(x:0, y:0, width:400, height:400)) | |
private var imageViewInitialized: Bool = false | |
@IBOutlet weak var resumeButton: UIButton! | |
@IBOutlet weak var cameraUnavailableLabel: UILabel! | |
@IBOutlet weak var tableView: UITableView! | |
@IBOutlet weak var threadCountLabel: UILabel! | |
@IBOutlet weak var threadCountStepper: UIStepper! | |
@IBOutlet weak var delegatesControl: UISegmentedControl! | |
// MARK: ModelDataHandler traits | |
var threadCount: Int = Constants.defaultThreadCount | |
var delegate: Delegates = Constants.defaultDelegate | |
// MARK: Result Variables | |
// Inferenced data to render. | |
private var inferencedData: InferencedData? | |
// Minimum score to render the result. | |
private let minimumScore: Float = 0.5 | |
private var avg_latency: Double = 0.0 | |
// Relative location of `overlayView` to `previewView`. | |
private var overlayViewFrame: CGRect? | |
private var previewViewFrame: CGRect? | |
// MARK: Controllers that manage functionality | |
// Handles all the camera related functionality | |
private lazy var cameraCapture = CameraFeedManager(previewView: previewView) | |
// Handles all data preprocessing and makes calls to run inference. | |
private var modelDataHandler: ModelDataHandler? | |
// MARK: View Handling Methods | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
do { | |
modelDataHandler = try ModelDataHandler() | |
} catch let error { | |
fatalError(error.localizedDescription) | |
} | |
cameraCapture.delegate = self | |
tableView.delegate = self | |
tableView.dataSource = self | |
// MARK: UI Initialization | |
// Setup thread count stepper with white color. | |
// https://forums.developer.apple.com/thread/121495 | |
threadCountStepper.setDecrementImage( | |
threadCountStepper.decrementImage(for: .normal), for: .normal) | |
threadCountStepper.setIncrementImage( | |
threadCountStepper.incrementImage(for: .normal), for: .normal) | |
// Setup initial stepper value and its label. | |
threadCountStepper.value = Double(Constants.defaultThreadCount) | |
threadCountLabel.text = Constants.defaultThreadCount.description | |
// Setup segmented controller's color. | |
delegatesControl.setTitleTextAttributes( | |
[NSAttributedString.Key.foregroundColor: UIColor.lightGray], | |
for: .normal) | |
delegatesControl.setTitleTextAttributes( | |
[NSAttributedString.Key.foregroundColor: UIColor.black], | |
for: .selected) | |
// Remove existing segments to initialize it with `Delegates` entries. | |
delegatesControl.removeAllSegments() | |
Delegates.allCases.forEach { delegate in | |
delegatesControl.insertSegment( | |
withTitle: delegate.description, | |
at: delegate.rawValue, | |
animated: false) | |
} | |
delegatesControl.selectedSegmentIndex = 0 | |
} | |
override func viewWillAppear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
cameraCapture.checkCameraConfigurationAndStartSession() | |
} | |
override func viewWillDisappear(_ animated: Bool) { | |
cameraCapture.stopSession() | |
} | |
override func viewDidLayoutSubviews() { | |
overlayViewFrame = overlayView.frame | |
previewViewFrame = previewView.frame | |
} | |
// MARK: Button Actions | |
@IBAction func didChangeThreadCount(_ sender: UIStepper) { | |
let changedCount = Int(sender.value) | |
if threadCountLabel.text == changedCount.description { | |
return | |
} | |
do { | |
modelDataHandler = try ModelDataHandler(threadCount: changedCount, delegate: delegate) | |
} catch let error { | |
fatalError(error.localizedDescription) | |
} | |
threadCount = changedCount | |
threadCountLabel.text = changedCount.description | |
os_log("Thread count is changed to: %d", threadCount) | |
} | |
@IBAction func didChangeDelegate(_ sender: UISegmentedControl) { | |
guard let changedDelegate = Delegates(rawValue: delegatesControl.selectedSegmentIndex) else { | |
fatalError("Unexpected value from delegates segemented controller.") | |
} | |
do { | |
modelDataHandler = try ModelDataHandler(threadCount: threadCount, delegate: changedDelegate) | |
} catch let error { | |
fatalError(error.localizedDescription) | |
} | |
delegate = changedDelegate | |
os_log("Delegate is changed to: %s", delegate.description) | |
} | |
@IBAction func didTapResumeButton(_ sender: Any) { | |
cameraCapture.resumeInterruptedSession { complete in | |
if complete { | |
self.resumeButton.isHidden = true | |
self.cameraUnavailableLabel.isHidden = true | |
} else { | |
self.presentUnableToResumeSessionAlert() | |
} | |
} | |
} | |
func presentUnableToResumeSessionAlert() { | |
let alert = UIAlertController( | |
title: "Unable to Resume Session", | |
message: "There was an error while attempting to resume session.", | |
preferredStyle: .alert | |
) | |
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) | |
self.present(alert, animated: true) | |
} | |
} | |
// MARK: - CameraFeedManagerDelegate Methods | |
extension ViewController: CameraFeedManagerDelegate { | |
func cameraFeedManager(_ manager: CameraFeedManager, didOutput pixelBuffer: CVPixelBuffer) { | |
runModel(on: pixelBuffer) | |
} | |
// MARK: Session Handling Alerts | |
func cameraFeedManagerDidEncounterSessionRunTimeError(_ manager: CameraFeedManager) { | |
// Handles session run time error by updating the UI and providing a button if session can be | |
// manually resumed. | |
self.resumeButton.isHidden = false | |
} | |
func cameraFeedManager( | |
_ manager: CameraFeedManager, sessionWasInterrupted canResumeManually: Bool | |
) { | |
// Updates the UI when session is interupted. | |
if canResumeManually { | |
self.resumeButton.isHidden = false | |
} else { | |
self.cameraUnavailableLabel.isHidden = false | |
} | |
} | |
func cameraFeedManagerDidEndSessionInterruption(_ manager: CameraFeedManager) { | |
// Updates UI once session interruption has ended. | |
self.cameraUnavailableLabel.isHidden = true | |
self.resumeButton.isHidden = true | |
} | |
func presentVideoConfigurationErrorAlert(_ manager: CameraFeedManager) { | |
let alertController = UIAlertController( | |
title: "Confirguration Failed", message: "Configuration of camera has failed.", | |
preferredStyle: .alert) | |
let okAction = UIAlertAction(title: "OK", style: .cancel, handler: nil) | |
alertController.addAction(okAction) | |
present(alertController, animated: true, completion: nil) | |
} | |
func presentCameraPermissionsDeniedAlert(_ manager: CameraFeedManager) { | |
let alertController = UIAlertController( | |
title: "Camera Permissions Denied", | |
message: | |
"Camera permissions have been denied for this app. You can change this by going to Settings", | |
preferredStyle: .alert) | |
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) | |
let settingsAction = UIAlertAction(title: "Settings", style: .default) { action in | |
if let url = URL.init(string: UIApplication.openSettingsURLString) { | |
UIApplication.shared.open(url, options: [:], completionHandler: nil) | |
} | |
} | |
alertController.addAction(cancelAction) | |
alertController.addAction(settingsAction) | |
present(alertController, animated: true, completion: nil) | |
} | |
@objc func runModel(on pixelBuffer: CVPixelBuffer) { | |
guard let overlayViewFrame = overlayViewFrame, let previewViewFrame = previewViewFrame | |
else { | |
return | |
} | |
// To put `overlayView` area as model input, transform `overlayViewFrame` following transform | |
// from `previewView` to `pixelBuffer`. `previewView` area is transformed to fit in | |
// `pixelBuffer`, because `pixelBuffer` as a camera output is resized to fill `previewView`. | |
// https://developer.apple.com/documentation/avfoundation/avlayervideogravity/1385607-resizeaspectfill | |
let modelInputRange = overlayViewFrame.applying( | |
previewViewFrame.size.transformKeepAspect(toFitIn: pixelBuffer.size)) | |
// Run Midas model. | |
guard | |
let (result, width, height, times) = self.modelDataHandler?.runMidas( | |
on: pixelBuffer, | |
from: modelInputRange, | |
to: overlayViewFrame.size) | |
else { | |
os_log("Cannot get inference result.", type: .error) | |
return | |
} | |
if avg_latency == 0 { | |
avg_latency = times.inference | |
} else { | |
avg_latency = times.inference*0.1 + avg_latency*0.9 | |
} | |
// Udpate `inferencedData` to render data in `tableView`. | |
inferencedData = InferencedData(score: Float(avg_latency), times: times) | |
//let height = 256 | |
//let width = 256 | |
let outputs = result | |
let outputs_size = width * height; | |
var multiplier : Float = 1.0; | |
let max_val : Float = outputs.max() ?? 0 | |
let min_val : Float = outputs.min() ?? 0 | |
if((max_val - min_val) > 0) { | |
multiplier = 255 / (max_val - min_val); | |
} | |
// Draw result. | |
DispatchQueue.main.async { | |
self.tableView.reloadData() | |
var pixels: [PixelData] = .init(repeating: .init(a: 255, r: 0, g: 0, b: 0), count: width * height) | |
for i in pixels.indices { | |
//if(i < 1000) | |
//{ | |
let val = UInt8((outputs[i] - min_val) * multiplier) | |
pixels[i].r = val | |
pixels[i].g = val | |
pixels[i].b = val | |
//} | |
} | |
/* | |
pixels[i].a = 255 | |
pixels[i].r = .random(in: 0...255) | |
pixels[i].g = .random(in: 0...255) | |
pixels[i].b = .random(in: 0...255) | |
} | |
*/ | |
DispatchQueue.main.async { | |
let image = UIImage(pixels: pixels, width: width, height: height) | |
self.imageView.image = image | |
if (self.imageViewInitialized == false) { | |
self.imageViewInitialized = true | |
self.overlayView.addSubview(self.imageView) | |
self.overlayView.setNeedsDisplay() | |
} | |
} | |
/* | |
let image = UIImage(pixels: pixels, width: width, height: height) | |
var imageView : UIImageView | |
imageView = UIImageView(frame:CGRect(x:0, y:0, width:400, height:400)); | |
imageView.image = image | |
self.overlayView.addSubview(imageView) | |
self.overlayView.setNeedsDisplay() | |
*/ | |
} | |
} | |
/* | |
func drawResult(of result: Result) { | |
self.overlayView.dots = result.dots | |
self.overlayView.lines = result.lines | |
self.overlayView.setNeedsDisplay() | |
} | |
func clearResult() { | |
self.overlayView.clear() | |
self.overlayView.setNeedsDisplay() | |
} | |
*/ | |
} | |
// MARK: - TableViewDelegate, TableViewDataSource Methods | |
extension ViewController: UITableViewDelegate, UITableViewDataSource { | |
func numberOfSections(in tableView: UITableView) -> Int { | |
return InferenceSections.allCases.count | |
} | |
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
guard let section = InferenceSections(rawValue: section) else { | |
return 0 | |
} | |
return section.subcaseCount | |
} | |
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
let cell = tableView.dequeueReusableCell(withIdentifier: "InfoCell") as! InfoCell | |
guard let section = InferenceSections(rawValue: indexPath.section) else { | |
return cell | |
} | |
guard let data = inferencedData else { return cell } | |
var fieldName: String | |
var info: String | |
switch section { | |
case .Score: | |
fieldName = section.description | |
info = String(format: "%.3f", data.score) | |
case .Time: | |
guard let row = ProcessingTimes(rawValue: indexPath.row) else { | |
return cell | |
} | |
var time: Double | |
switch row { | |
case .InferenceTime: | |
time = data.times.inference | |
} | |
fieldName = row.description | |
info = String(format: "%.2fms", time) | |
} | |
cell.fieldNameLabel.text = fieldName | |
cell.infoLabel.text = info | |
return cell | |
} | |
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { | |
guard let section = InferenceSections(rawValue: indexPath.section) else { | |
return 0 | |
} | |
var height = Traits.normalCellHeight | |
if indexPath.row == section.subcaseCount - 1 { | |
height = Traits.separatorCellHeight + Traits.bottomSpacing | |
} | |
return height | |
} | |
} | |
// MARK: - Private enums | |
/// UI coinstraint values | |
fileprivate enum Traits { | |
static let normalCellHeight: CGFloat = 35.0 | |
static let separatorCellHeight: CGFloat = 25.0 | |
static let bottomSpacing: CGFloat = 30.0 | |
} | |
fileprivate struct InferencedData { | |
var score: Float | |
var times: Times | |
} | |
/// Type of sections in Info Cell | |
fileprivate enum InferenceSections: Int, CaseIterable { | |
case Score | |
case Time | |
var description: String { | |
switch self { | |
case .Score: | |
return "Average" | |
case .Time: | |
return "Processing Time" | |
} | |
} | |
var subcaseCount: Int { | |
switch self { | |
case .Score: | |
return 1 | |
case .Time: | |
return ProcessingTimes.allCases.count | |
} | |
} | |
} | |
/// Type of processing times in Time section in Info Cell | |
fileprivate enum ProcessingTimes: Int, CaseIterable { | |
case InferenceTime | |
var description: String { | |
switch self { | |
case .InferenceTime: | |
return "Inference Time" | |
} | |
} | |
} | |