CaptureSessionManager.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. //
  2. // CaptureManager.swift
  3. // WeScan
  4. //
  5. // Created by Boris Emorine on 2/8/18.
  6. // Copyright © 2018 WeTransfer. All rights reserved.
  7. //
  8. import Foundation
  9. import AVFoundation
  10. @available(iOS 10, *)
  11. /// A set of functions that inform the delegate object of the state of the detection.
  12. protocol RectangleDetectionDelegateProtocol: NSObjectProtocol {
  13. /// Called when the capture of a picture has started.
  14. ///
  15. /// - Parameters:
  16. /// - captureSessionManager: The `CaptureSessionManager` instance that started capturing a picture.
  17. func didStartCapturingPicture(for captureSessionManager: CaptureSessionManager)
  18. /// Called when a quadrilateral has been detected.
  19. /// - Parameters:
  20. /// - captureSessionManager: The `CaptureSessionManager` instance that has detected a quadrilateral.
  21. /// - quad: The detected quadrilateral in the coordinates of the image.
  22. /// - imageSize: The size of the image the quadrilateral has been detected on.
  23. func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didDetectQuad quad: Quadrilateral?, _ imageSize: CGSize)
  24. /// Called when a picture with or without a quadrilateral has been captured.
  25. ///
  26. /// - Parameters:
  27. /// - captureSessionManager: The `CaptureSessionManager` instance that has captured a picture.
  28. /// - picture: The picture that has been captured.
  29. /// - quad: The quadrilateral that was detected in the picture's coordinates if any.
  30. func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didCapturePicture picture: UIImage, withQuad quad: Quadrilateral?)
  31. /// Called when an error occured with the capture session manager.
  32. /// - Parameters:
  33. /// - captureSessionManager: The `CaptureSessionManager` that encountered an error.
  34. /// - error: The encountered error.
  35. func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didFailWithError error: Error)
  36. }
  37. @available(iOS 10, *)
  38. /// The CaptureSessionManager is responsible for setting up and managing the AVCaptureSession and the functions related to capturing.
  39. final class CaptureSessionManager: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
  40. private let videoPreviewLayer: AVCaptureVideoPreviewLayer
  41. private let captureSession = AVCaptureSession()
  42. private let rectangleFunnel = RectangleFeaturesFunnel()
  43. weak var delegate: RectangleDetectionDelegateProtocol?
  44. private var displayedRectangleResult: RectangleDetectorResult?
  45. private var photoOutput = AVCapturePhotoOutput()
  46. /// Whether the CaptureSessionManager should be detecting quadrilaterals.
  47. private var isDetecting = true
  48. /// The number of times no rectangles have been found in a row.
  49. private var noRectangleCount = 0
  50. /// The minimum number of time required by `noRectangleCount` to validate that no rectangles have been found.
  51. private let noRectangleThreshold = 3
  52. // MARK: Life Cycle
  53. init?(videoPreviewLayer: AVCaptureVideoPreviewLayer) {
  54. self.videoPreviewLayer = videoPreviewLayer
  55. super.init()
  56. captureSession.beginConfiguration()
  57. captureSession.sessionPreset = AVCaptureSession.Preset.photo
  58. photoOutput.isHighResolutionCaptureEnabled = true
  59. let videoOutput = AVCaptureVideoDataOutput()
  60. videoOutput.alwaysDiscardsLateVideoFrames = true
  61. guard let inputDevice = AVCaptureDevice.default(for: AVMediaType.video),
  62. let deviceInput = try? AVCaptureDeviceInput(device: inputDevice),
  63. captureSession.canAddInput(deviceInput),
  64. captureSession.canAddOutput(photoOutput),
  65. captureSession.canAddOutput(videoOutput) else {
  66. let error = ImageScannerControllerError.inputDevice
  67. delegate?.captureSessionManager(self, didFailWithError: error)
  68. return
  69. }
  70. captureSession.addInput(deviceInput)
  71. captureSession.addOutput(photoOutput)
  72. captureSession.addOutput(videoOutput)
  73. videoPreviewLayer.session = captureSession
  74. videoPreviewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
  75. videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "video_ouput_queue"))
  76. captureSession.commitConfiguration()
  77. }
  78. // MARK: Capture Session Life Cycle
  79. /// Starts the camera and detecting quadrilaterals.
  80. internal func start() {
  81. let authorizationStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)
  82. switch authorizationStatus {
  83. case .authorized:
  84. self.captureSession.startRunning()
  85. isDetecting = true
  86. case .notDetermined:
  87. AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (_) in
  88. DispatchQueue.main.async { [weak self] in
  89. self?.start()
  90. }
  91. })
  92. default:
  93. let error = ImageScannerControllerError.authorization
  94. delegate?.captureSessionManager(self, didFailWithError: error)
  95. }
  96. }
  97. internal func stop() {
  98. captureSession.stopRunning()
  99. }
  100. internal func capturePhoto() {
  101. let photoSettings = AVCapturePhotoSettings()
  102. photoSettings.isHighResolutionPhotoEnabled = true
  103. photoSettings.isAutoStillImageStabilizationEnabled = true
  104. if let photoOutputConnection = self.photoOutput.connection(with: .video) {
  105. photoOutputConnection.videoOrientation = AVCaptureVideoOrientation(deviceOrientation: UIDevice.current.orientation) ?? AVCaptureVideoOrientation.portrait
  106. }
  107. photoOutput.capturePhoto(with: photoSettings, delegate: self)
  108. }
  109. // MARK: - AVCaptureVideoDataOutputSampleBufferDelegate
  110. func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  111. guard isDetecting == true else {
  112. return
  113. }
  114. guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
  115. return
  116. }
  117. let videoOutputImage = CIImage(cvPixelBuffer: pixelBuffer)
  118. let imageSize = videoOutputImage.extent.size
  119. guard let rectangle = RectangleDetector.rectangle(forImage: videoOutputImage) else {
  120. DispatchQueue.main.async { [weak self] in
  121. guard let strongSelf = self else {
  122. return
  123. }
  124. strongSelf.noRectangleCount += 1
  125. if strongSelf.noRectangleCount > strongSelf.noRectangleThreshold {
  126. strongSelf.displayedRectangleResult = nil
  127. strongSelf.delegate?.captureSessionManager(strongSelf, didDetectQuad: nil, imageSize)
  128. }
  129. }
  130. return
  131. }
  132. noRectangleCount = 0
  133. rectangleFunnel.add(rectangle, currentlyDisplayedRectangle: displayedRectangleResult?.rectangle) { (rectangle) in
  134. displayRectangleResult(rectangleResult: RectangleDetectorResult(rectangle: rectangle, imageSize: imageSize))
  135. }
  136. }
  137. @discardableResult private func displayRectangleResult(rectangleResult: RectangleDetectorResult) -> Quadrilateral {
  138. displayedRectangleResult = rectangleResult
  139. let quad = Quadrilateral(rectangleFeature: rectangleResult.rectangle).toCartesian(withHeight: rectangleResult.imageSize.height)
  140. DispatchQueue.main.async { [weak self] in
  141. guard let strongSelf = self else {
  142. return
  143. }
  144. strongSelf.delegate?.captureSessionManager(strongSelf, didDetectQuad: quad, rectangleResult.imageSize)
  145. }
  146. return quad
  147. }
  148. }
  149. @available(iOS 10, *)
  150. extension CaptureSessionManager: AVCapturePhotoCaptureDelegate {
  151. func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
  152. if let error = error {
  153. delegate?.captureSessionManager(self, didFailWithError: error)
  154. return
  155. }
  156. isDetecting = false
  157. delegate?.didStartCapturingPicture(for: self)
  158. if let sampleBuffer = photoSampleBuffer,
  159. let imageData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: sampleBuffer, previewPhotoSampleBuffer: nil) {
  160. completeImageCapture(with: imageData)
  161. } else {
  162. let error = ImageScannerControllerError.capture
  163. delegate?.captureSessionManager(self, didFailWithError: error)
  164. return
  165. }
  166. }
  167. @available(iOS 11.0, *)
  168. func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
  169. if let error = error {
  170. delegate?.captureSessionManager(self, didFailWithError: error)
  171. return
  172. }
  173. isDetecting = false
  174. delegate?.didStartCapturingPicture(for: self)
  175. if let imageData = photo.fileDataRepresentation() {
  176. completeImageCapture(with: imageData)
  177. } else {
  178. let error = ImageScannerControllerError.capture
  179. delegate?.captureSessionManager(self, didFailWithError: error)
  180. return
  181. }
  182. }
  183. /// Completes the image capture by processing the image, and passing it to the delegate object.
  184. /// This function is necessary because the capture functions for iOS 10 and 11 are decoupled.
  185. private func completeImageCapture(with imageData: Data) {
  186. DispatchQueue.global(qos: .background).async { [weak self] in
  187. guard let image = UIImage(data: imageData) else {
  188. let error = ImageScannerControllerError.capture
  189. DispatchQueue.main.async {
  190. guard let strongSelf = self else {
  191. return
  192. }
  193. strongSelf.delegate?.captureSessionManager(strongSelf, didFailWithError: error)
  194. }
  195. return
  196. }
  197. var angle: CGFloat = 0.0
  198. switch image.imageOrientation {
  199. case .right:
  200. angle = CGFloat.pi / 2
  201. case .up:
  202. angle = CGFloat.pi
  203. default:
  204. break
  205. }
  206. var quad: Quadrilateral?
  207. if let displayedRectangleResult = self?.displayedRectangleResult {
  208. quad = self?.displayRectangleResult(rectangleResult: displayedRectangleResult)
  209. quad = quad?.scale(displayedRectangleResult.imageSize, image.size, withRotationAngle: angle)
  210. }
  211. DispatchQueue.main.async {
  212. guard let strongSelf = self else {
  213. return
  214. }
  215. strongSelf.delegate?.captureSessionManager(strongSelf, didCapturePicture: image, withQuad: quad)
  216. }
  217. }
  218. }
  219. }
  220. /// Data structure representing the result of the detection of a quadrilateral.
  221. fileprivate struct RectangleDetectorResult {
  222. /// The detected quadrilateral.
  223. let rectangle: CIRectangleFeature
  224. /// The size of the image the quadrilateral was detected on.
  225. let imageSize: CGSize
  226. }