@@ -19,9 +19,15 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
1919 // Image processing
2020 private let context = CIContext ( )
2121
22- // Face detection
23- private var faceDetectionRequest : VNDetectFaceRectanglesRequest !
24- private var detectedFaces : [ VNFaceObservation ] = [ ]
22+ // Person segmentation
23+ private var personSegmentationRequest : VNGeneratePersonSegmentationRequest !
24+ private var personMaskBuffer : CVPixelBuffer ?
25+
26+ #if targetEnvironment(simulator)
27+ private var simulatorPlayer : AVPlayer ?
28+ private var simulatorVideoOutput : AVPlayerItemVideoOutput ?
29+ private var simulatorDisplayLink : CADisplayLink ?
30+ #endif
2531
2632 // Video Recording
2733 private var assetWriter : AVAssetWriter ?
@@ -66,7 +72,7 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
6672 setupTimerLabel ( )
6773 setupConnectionStatusIcon ( )
6874 setupCamera ( )
69- setupFaceDetection ( )
75+ setupPrivacyFiltering ( )
7076
7177 // ConnectionManager is a singleton, so we just ensure it's connected and observe it
7278 ConnectionManager . shared. connect ( )
@@ -93,9 +99,14 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
9399
94100 stopRecording ( )
95101
102+ #if targetEnvironment(simulator)
103+ simulatorPlayer? . pause ( )
104+ simulatorDisplayLink? . isPaused = true
105+ #else
96106 if let session = captureSession, session. isRunning {
97- session. stopRunning ( )
107+ session. stopRunning ( )
98108 }
109+ #endif
99110
100111 // Close WebSocket connection
101112 // WebSocket connection is now managed by ConnectionManager (background), so we don't disconnect here.
@@ -397,6 +408,7 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
397408 }
398409
399410 private func configureCameraSwitch( ) {
411+ guard captureSession != nil else { return }
400412 captureSession. beginConfiguration ( )
401413
402414 // Remove existing video input
@@ -750,10 +762,11 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
750762
751763 private func setupCamera( ) {
752764 // Camera initialization will be handled in checkAndRequestPermissions
753- setupFaceDetection ( )
765+ setupPrivacyFiltering ( )
754766 }
755767
756768 private func setupAudioInput( ) {
769+ guard captureSession != nil else { return }
757770 // Check if audio input already exists
758771 let audioInputExists = captureSession. inputs. contains { input in
759772 guard let deviceInput = input as? AVCaptureDeviceInput else { return false }
@@ -835,6 +848,15 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
835848 }
836849
837850 private func initializeCameraIfNeeded( ) {
851+ #if targetEnvironment(simulator)
852+ guard simulatorPlayer == nil else {
853+ simulatorPlayer? . play ( )
854+ simulatorDisplayLink? . isPaused = false
855+ return
856+ }
857+ setupSimulatorVideoLoop ( )
858+ return
859+ #else
838860 guard captureSession == nil else {
839861 // Camera already initialized, just start running
840862 if !captureSession. isRunning {
@@ -886,6 +908,7 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
886908
887909 // Reconnect WebSocket logic is now handled by ConnectionManager
888910
911+ #endif
889912 }
890913
891914 private func showPermissionDeniedAlert( permissionType: String ) {
@@ -900,6 +923,7 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
900923 }
901924
902925 private func updateVideoOrientation( ) {
926+ guard captureSession != nil else { return }
903927 // Find the video input, not just the first input (which could be audio)
904928 if let videoCaptureDevice = captureSession. inputs. compactMap ( { $0 as? AVCaptureDeviceInput } ) . first ( where: { $0. device. hasMediaType ( . video) } ) {
905929 // Initialize RotationCoordinator if needed
@@ -935,60 +959,109 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
935959 }
936960 }
937961
938- private func setupFaceDetection( ) {
939- faceDetectionRequest = VNDetectFaceRectanglesRequest { [ weak self] request, error in
940- guard let observations = request. results as? [ VNFaceObservation ] else {
941- return
942- }
962+ private func setupPrivacyFiltering( ) {
963+ let request = VNGeneratePersonSegmentationRequest ( )
964+ request. qualityLevel = . fast
965+ request. outputPixelFormat = kCVPixelFormatType_OneComponent8
966+ self . personSegmentationRequest = request
967+ }
943968
944- self ? . detectedFaces = observations
969+ private func applyPrivacyBlur( to image: CIImage ) -> CIImage {
970+ guard let maskBuffer = personMaskBuffer else { return image }
971+
972+ let maskImage = CIImage ( cvPixelBuffer: maskBuffer)
973+
974+ // Ensure accurate bounding box alignment by scaling
975+ let scaleX = image. extent. width / maskImage. extent. width
976+ let scaleY = image. extent. height / maskImage. extent. height
977+ let scaledMask = maskImage. transformed ( by: CGAffineTransform ( scaleX: scaleX, y: scaleY) )
978+
979+ // Pixellate filter to anonymize
980+ if let blurFilter = CIFilter ( name: " CIPixellate " ) {
981+ blurFilter. setValue ( image, forKey: kCIInputImageKey)
982+ blurFilter. setValue ( 40.0 , forKey: kCIInputScaleKey)
983+
984+ if let blurredImage = blurFilter. outputImage {
985+ if let blendFilter = CIFilter ( name: " CIBlendWithMask " ) {
986+ blendFilter. setValue ( blurredImage, forKey: kCIInputImageKey)
987+ blendFilter. setValue ( image, forKey: kCIInputBackgroundImageKey)
988+ blendFilter. setValue ( scaledMask, forKey: kCIInputMaskImageKey)
989+
990+ if let output = blendFilter. outputImage {
991+ return output
992+ }
993+ }
994+ }
945995 }
996+ return image
946997 }
947998
948- private func blurFaces( in ciImage: CIImage , faces: [ VNFaceObservation ] ) -> CIImage {
949- var outputImage = ciImage
950- let imageSize = ciImage. extent. size
951-
952- for face in faces {
953- // Convert normalized coordinates to image coordinates
954- // Vision uses normalized coordinates (0-1) with origin at bottom-left
955- let boundingBox = face. boundingBox
956-
957- // Convert from Vision coordinates to CIImage coordinates
958- let x = boundingBox. origin. x * imageSize. width
959- let y = boundingBox. origin. y * imageSize. height
960- let width = boundingBox. width * imageSize. width
961- let height = boundingBox. height * imageSize. height
962-
963- // Expand the box slightly for better coverage
964- let expansion : CGFloat = 0.3
965- let expandedX = max ( 0 , x - width * expansion)
966- let expandedY = max ( 0 , y - height * expansion)
967- let expandedWidth = min ( imageSize. width - expandedX, width * ( 1 + 2 * expansion) )
968- let expandedHeight = min ( imageSize. height - expandedY, height * ( 1 + 2 * expansion) )
999+ #if targetEnvironment(simulator)
1000+ private func setupSimulatorVideoLoop( ) {
1001+ let documentsPath = FileManager . default. urls ( for: . documentDirectory, in: . userDomainMask) [ 0 ]
1002+ let videoURL = documentsPath. appendingPathComponent ( " demo_video.mp4 " )
9691003
970- let faceRect = CGRect ( x: expandedX, y: expandedY, width: expandedWidth, height: expandedHeight)
1004+ guard FileManager . default. fileExists ( atPath: videoURL. path) else {
1005+ DispatchQueue . main. async {
1006+ let alert = UIAlertController ( title: " Simulator Video Missing " , message: " Please drag 'demo_video.mp4' into the iOS Simulator's AllSpark-ios folder using the macOS Finder or Simulator Files app. " , preferredStyle: . alert)
1007+ alert. addAction ( UIAlertAction ( title: " OK " , style: . default) )
1008+ self . present ( alert, animated: true )
1009+ }
1010+ return
1011+ }
9711012
972- // Create blur filter
973- if let blurFilter = CIFilter ( name: " CIPixellate " ) {
974- // Crop the face region
975- let faceCrop = outputImage. cropped ( to: faceRect)
1013+ let player = AVPlayer ( url: videoURL)
1014+ player. actionAtItemEnd = . none
9761015
977- blurFilter. setValue ( faceCrop, forKey: kCIInputImageKey)
978- blurFilter. setValue ( 40.0 , forKey: kCIInputScaleKey)
1016+ let output = AVPlayerItemVideoOutput ( pixelBufferAttributes: [
1017+ kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_32BGRA
1018+ ] )
1019+ player. currentItem? . add ( output)
1020+
1021+ simulatorPlayer = player
1022+ simulatorVideoOutput = output
1023+
1024+ NotificationCenter . default. addObserver ( forName: . AVPlayerItemDidPlayToEndTime, object: player. currentItem, queue: . main) { _ in
1025+ player. seek ( to: . zero)
1026+ player. play ( )
1027+ }
9791028
980- if let blurredOutput = blurFilter. outputImage {
981- // The blur filter expands the image, so we need to crop it back
982- let croppedBlur = blurredOutput. cropped ( to: faceRect)
1029+ simulatorDisplayLink = CADisplayLink ( target: self , selector: #selector( simulatorDisplayLinkFired) )
1030+ simulatorDisplayLink? . add ( to: . main, forMode: . common)
1031+
1032+ player. play ( )
1033+ }
9831034
984- // Composite the blurred face back onto the original image
985- outputImage = croppedBlur. composited ( over: outputImage)
986- }
1035+ @objc private func simulatorDisplayLinkFired( ) {
1036+ guard let output = simulatorVideoOutput else { return }
1037+ let itemTime = output. itemTime ( forHostTime: CACurrentMediaTime ( ) )
1038+ guard output. hasNewPixelBuffer ( forItemTime: itemTime) else { return }
1039+
1040+ var presentationItemTime = CMTime . zero
1041+ guard let pixelBuffer = output. copyPixelBuffer ( forItemTime: itemTime, itemTimeForDisplay: & presentationItemTime) else { return }
1042+
1043+ let ciImage = CIImage ( cvPixelBuffer: pixelBuffer)
1044+
1045+ let handler = VNImageRequestHandler ( ciImage: ciImage, orientation: . up, options: [ : ] )
1046+ do {
1047+ try handler. perform ( [ personSegmentationRequest] )
1048+ if let result = personSegmentationRequest. results? . first as? VNPixelBufferObservation {
1049+ self . personMaskBuffer = result. pixelBuffer
9871050 }
1051+ } catch { }
1052+
1053+ let processedImage = applyPrivacyBlur ( to: ciImage)
1054+
1055+ // Use monotonic host clock because video file will randomly jump to 0.0s causing AVAssetWriter to fail
1056+ let currentHostTime = CMClockGetTime ( CMClockGetHostTimeClock ( ) )
1057+ recordVideoFrame ( processedImage, timestamp: currentHostTime)
1058+
1059+ if let cgImage = context. createCGImage ( processedImage, from: processedImage. extent) {
1060+ let uiImage = UIImage ( cgImage: cgImage)
1061+ self . imageView. image = uiImage
9881062 }
989-
990- return outputImage
9911063 }
1064+ #endif
9921065
9931066 private func recordAudioFrame( _ sampleBuffer: CMSampleBuffer ) {
9941067 recordingStateLock. lock ( )
@@ -1128,17 +1201,20 @@ extension CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate, AV
11281201
11291202 let ciImage = CIImage ( cvPixelBuffer: pixelBuffer)
11301203
1131- // Perform face detection
1204+ // Perform person segmentation
11321205 let handler = VNImageRequestHandler ( ciImage: ciImage, orientation: . up, options: [ : ] )
11331206
11341207 do {
1135- try handler. perform ( [ faceDetectionRequest] )
1208+ try handler. perform ( [ personSegmentationRequest] )
1209+ if let result = personSegmentationRequest. results? . first as? VNPixelBufferObservation {
1210+ self . personMaskBuffer = result. pixelBuffer
1211+ }
11361212 } catch {
1137- print ( " Failed to perform face detection : \( error) " )
1213+ print ( " Failed to perform person segmentation : \( error) " )
11381214 }
11391215
1140- // Apply blur to detected faces
1141- let processedImage = blurFaces ( in : ciImage, faces : detectedFaces )
1216+ // Apply blur to humans
1217+ let processedImage = applyPrivacyBlur ( to : ciImage)
11421218
11431219 // Record output
11441220 let timestamp = CMSampleBufferGetPresentationTimeStamp ( sampleBuffer)
0 commit comments