@@ -18,10 +18,15 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
1818
1919 // Image processing
2020 private let context = CIContext ( )
21-
22- // Person segmentation
21+
22+ // Privacy Filtering
23+ private var privacyMode : String = " segmentation "
2324 private var personSegmentationRequest : VNGeneratePersonSegmentationRequest !
2425 private var personMaskBuffer : CVPixelBuffer ?
26+
27+ // Body Pose Detection
28+ private var bodyPoseRequest : VNDetectHumanBodyPoseRequest !
29+ private var detectedBodyPoses : [ VNHumanBodyPoseObservation ] = [ ]
2530
2631#if targetEnvironment(simulator)
2732 private var simulatorPlayer : AVPlayer ?
@@ -177,34 +182,42 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
177182 iconView. contentMode = . scaleAspectFit
178183 stackView. addArrangedSubview ( iconView)
179184
185+ // Text Stack View for Timer and Modes Label
186+ let textStackView = UIStackView ( )
187+ textStackView. translatesAutoresizingMaskIntoConstraints = false
188+ textStackView. axis = . vertical
189+ textStackView. spacing = 2
190+ textStackView. alignment = . leading
191+
180192 // Timer Label
181193 timerLabel = UILabel ( )
182194 timerLabel. translatesAutoresizingMaskIntoConstraints = false
183195 timerLabel. text = " 00:00 "
184196 timerLabel. textColor = . red // User requested red
185197 timerLabel. font = UIFont . monospacedDigitSystemFont ( ofSize: 20 , weight: . bold)
186- stackView . addArrangedSubview ( timerLabel)
187-
198+ textStackView . addArrangedSubview ( timerLabel)
199+
188200 captureModesLabel = UILabel ( )
189201 captureModesLabel. translatesAutoresizingMaskIntoConstraints = false
190202 captureModesLabel. text = " "
191203 captureModesLabel. textColor = . white
192- captureModesLabel. font = UIFont . systemFont ( ofSize: 12 , weight: . medium)
204+ captureModesLabel. font = UIFont . systemFont ( ofSize: 11 , weight: . medium)
193205 captureModesLabel. numberOfLines = 1
194- captureModesLabel. setContentHuggingPriority ( . defaultHigh, for: . horizontal)
195- stackView. addArrangedSubview ( captureModesLabel)
206+ textStackView. addArrangedSubview ( captureModesLabel)
207+
208+ stackView. addArrangedSubview ( textStackView)
196209
197210 NSLayoutConstraint . activate ( [
198211 recordingIndicatorContainer. topAnchor. constraint ( equalTo: view. safeAreaLayoutGuide. topAnchor, constant: 20 ) ,
199212 recordingIndicatorContainer. centerXAnchor. constraint ( equalTo: view. centerXAnchor) ,
200- // Height enough for padding
201- recordingIndicatorContainer. heightAnchor. constraint ( equalToConstant : 36 ) ,
213+ // Flex height based on content
214+ recordingIndicatorContainer. heightAnchor. constraint ( greaterThanOrEqualToConstant : 36 ) ,
202215
203216 // StackView constraints inside container with padding
204217 stackView. leadingAnchor. constraint ( equalTo: recordingIndicatorContainer. leadingAnchor, constant: 10 ) ,
205218 stackView. trailingAnchor. constraint ( equalTo: recordingIndicatorContainer. trailingAnchor, constant: - 10 ) ,
206- stackView. topAnchor. constraint ( equalTo: recordingIndicatorContainer. topAnchor) ,
207- stackView. bottomAnchor. constraint ( equalTo: recordingIndicatorContainer. bottomAnchor) ,
219+ stackView. topAnchor. constraint ( equalTo: recordingIndicatorContainer. topAnchor, constant : 6 ) ,
220+ stackView. bottomAnchor. constraint ( equalTo: recordingIndicatorContainer. bottomAnchor, constant : - 6 ) ,
208221
209222 // Icon size
210223 iconView. widthAnchor. constraint ( equalToConstant: 12 ) ,
@@ -491,8 +504,8 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
491504 // Get camera position for filename
492505 let cameraPosition = currentCameraPosition == . front ? " front " : " back "
493506
494- let timestamp = Date ( ) . timeIntervalSince1970
495- let videoName = " recording_ \( deviceNameForFilename ) _ \( cameraPosition ) _ \( timestamp ) . \( fileExtension) "
507+ let timestampMs = Int ( Date ( ) . timeIntervalSince1970 * 1000 )
508+ let videoName = " tmp_recording_ \( timestampMs ) . \( fileExtension) "
496509 videoURL = documentsPath. appendingPathComponent ( videoName)
497510
498511 guard let videoURL = videoURL else { return }
@@ -682,14 +695,14 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
682695 let format = url. pathExtension
683696 let newFilename = " chunk_ \( chunkTimeMs) . \( format) "
684697 let finalURL = documentsPath. appendingPathComponent ( newFilename)
685-
698+
686699 try ? FileManager . default. removeItem ( at: finalURL)
687700 do {
688701 try FileManager . default. moveItem ( at: url, to: finalURL)
689702 } catch {
690703 print ( " Error renaming video chunk: \( error) " )
691704 }
692-
705+
693706 // Write timestamps file
694707 let timestampsFilename = " timestamps_ \( chunkTimeMs) .txt "
695708 let timestampsURL = documentsPath. appendingPathComponent ( timestampsFilename)
@@ -702,7 +715,7 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
702715 // Determine camera from original name since we drop it in final name
703716 let parts = url. deletingPathExtension ( ) . lastPathComponent. components ( separatedBy: " _ " )
704717 let camera = parts. count > 2 ? parts [ parts. count - 2 ] : " unknown " // "front" or "back"
705-
718+
706719 let urlToProcess = finalURL
707720
708721 DispatchQueue . global ( qos: . utility) . async {
@@ -960,40 +973,88 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
960973 }
961974
962975 private func setupPrivacyFiltering( ) {
963- let request = VNGeneratePersonSegmentationRequest ( )
964- request. qualityLevel = . fast
965- request. outputPixelFormat = kCVPixelFormatType_OneComponent8
966- self . personSegmentationRequest = request
976+ self . privacyMode = UserDefaults . standard. string ( forKey: " privacyMode " ) ?? " segmentation "
977+
978+ let segRequest = VNGeneratePersonSegmentationRequest ( )
979+ segRequest. qualityLevel = . accurate
980+ self . personSegmentationRequest = segRequest
981+
982+ self . bodyPoseRequest = VNDetectHumanBodyPoseRequest ( )
967983 }
968984
969985 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)
986+ if privacyMode == " segmentation " {
987+ guard let maskBuffer = personMaskBuffer else { return image }
988+ let maskImage = CIImage ( cvPixelBuffer: maskBuffer)
989+
990+ let scaleX = image. extent. width / maskImage. extent. width
991+ let scaleY = image. extent. height / maskImage. extent. height
992+ let scaledMask = maskImage. transformed ( by: CGAffineTransform ( scaleX: scaleX, y: scaleY) )
983993
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
994+ if let blurFilter = CIFilter ( name: " CIGaussianBlur " ) {
995+ blurFilter. setValue ( image, forKey: kCIInputImageKey)
996+ blurFilter. setValue ( 30.0 , forKey: kCIInputRadiusKey)
997+ if let blurredImage = blurFilter. outputImage {
998+ let clampedBlur = blurredImage. cropped ( to: image. extent)
999+ if let blendFilter = CIFilter ( name: " CIBlendWithRedMask " ) {
1000+ blendFilter. setValue ( clampedBlur, forKey: kCIInputImageKey)
1001+ blendFilter. setValue ( image, forKey: kCIInputBackgroundImageKey)
1002+ blendFilter. setValue ( scaledMask, forKey: kCIInputMaskImageKey)
1003+ if let output = blendFilter. outputImage { return output }
1004+ }
1005+ }
1006+ }
1007+ return image
1008+ } else {
1009+ var outputImage = image
1010+ let imageSize = image. extent. size
1011+
1012+ for pose in detectedBodyPoses {
1013+ // Find bounding box for all confident body joints (like ankles, knees)
1014+ guard let points = try ? pose. recognizedPoints ( . all) else { continue }
1015+ var minX : CGFloat = 1.0 , minY : CGFloat = 1.0
1016+ var maxX : CGFloat = 0.0 , maxY : CGFloat = 0.0
1017+ var validPoints = 0
1018+
1019+ for (_, point) in points where point. confidence > 0.2 {
1020+ minX = min ( minX, point. location. x)
1021+ minY = min ( minY, point. location. y)
1022+ maxX = max ( maxX, point. location. x)
1023+ maxY = max ( maxY, point. location. y)
1024+ validPoints += 1
1025+ }
1026+
1027+ guard validPoints > 0 else { continue }
1028+
1029+ let x = minX * imageSize. width
1030+ let y = minY * imageSize. height
1031+ let width = ( maxX - minX) * imageSize. width
1032+ let height = ( maxY - minY) * imageSize. height
1033+
1034+ // Expand the box slightly for full coverage of the limbs
1035+ let expansion : CGFloat = 0.5
1036+ let expandedX = max ( 0 , x - width * expansion)
1037+ let expandedY = max ( 0 , y - height * expansion)
1038+ let expandedWidth = min ( imageSize. width - expandedX, width * ( 1 + 2 * expansion) )
1039+ let expandedHeight = min ( imageSize. height - expandedY, height * ( 1 + 2 * expansion) )
1040+
1041+ let poseRect = CGRect ( x: expandedX, y: expandedY, width: expandedWidth, height: expandedHeight)
1042+
1043+ // Blur the bounding box
1044+ if let blurFilter = CIFilter ( name: " CIGaussianBlur " ) {
1045+ let poseCrop = outputImage. cropped ( to: poseRect)
1046+ blurFilter. setValue ( poseCrop, forKey: kCIInputImageKey)
1047+ blurFilter. setValue ( 30.0 , forKey: kCIInputRadiusKey)
1048+
1049+ if let blurredOutput = blurFilter. outputImage {
1050+ let croppedBlur = blurredOutput. cropped ( to: poseRect)
1051+ outputImage = croppedBlur. composited ( over: outputImage)
9921052 }
9931053 }
9941054 }
1055+
1056+ return outputImage
9951057 }
996- return image
9971058 }
9981059
9991060#if targetEnvironment(simulator)
@@ -1017,45 +1078,50 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
10171078 kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_32BGRA
10181079 ] )
10191080 player. currentItem? . add ( output)
1020-
1081+
10211082 simulatorPlayer = player
10221083 simulatorVideoOutput = output
1023-
1084+
10241085 NotificationCenter . default. addObserver ( forName: . AVPlayerItemDidPlayToEndTime, object: player. currentItem, queue: . main) { _ in
10251086 player. seek ( to: . zero)
10261087 player. play ( )
10271088 }
10281089
10291090 simulatorDisplayLink = CADisplayLink ( target: self , selector: #selector( simulatorDisplayLinkFired) )
10301091 simulatorDisplayLink? . add ( to: . main, forMode: . common)
1031-
1092+
10321093 player. play ( )
10331094 }
10341095
10351096 @objc private func simulatorDisplayLinkFired( ) {
10361097 guard let output = simulatorVideoOutput else { return }
10371098 let itemTime = output. itemTime ( forHostTime: CACurrentMediaTime ( ) )
10381099 guard output. hasNewPixelBuffer ( forItemTime: itemTime) else { return }
1039-
1100+
10401101 var presentationItemTime = CMTime . zero
10411102 guard let pixelBuffer = output. copyPixelBuffer ( forItemTime: itemTime, itemTimeForDisplay: & presentationItemTime) else { return }
1042-
1103+
10431104 let ciImage = CIImage ( cvPixelBuffer: pixelBuffer)
1044-
1105+
10451106 let handler = VNImageRequestHandler ( ciImage: ciImage, orientation: . up, options: [ : ] )
10461107 do {
1047- try handler. perform ( [ personSegmentationRequest] )
1048- if let result = personSegmentationRequest. results? . first as? VNPixelBufferObservation {
1049- self . personMaskBuffer = result. pixelBuffer
1108+ if privacyMode == " segmentation " {
1109+ try handler. perform ( [ personSegmentationRequest] )
1110+ if let result = personSegmentationRequest. results? . first as? VNPixelBufferObservation {
1111+ self . personMaskBuffer = result. pixelBuffer
1112+ }
1113+ } else {
1114+ try handler. perform ( [ bodyPoseRequest] )
1115+ self . detectedBodyPoses = bodyPoseRequest. results ?? [ ]
10501116 }
10511117 } catch { }
1052-
1118+
10531119 let processedImage = applyPrivacyBlur ( to: ciImage)
1054-
1120+
10551121 // Use monotonic host clock because video file will randomly jump to 0.0s causing AVAssetWriter to fail
10561122 let currentHostTime = CMClockGetTime ( CMClockGetHostTimeClock ( ) )
10571123 recordVideoFrame ( processedImage, timestamp: currentHostTime)
1058-
1124+
10591125 if let cgImage = context. createCGImage ( processedImage, from: processedImage. extent) {
10601126 let uiImage = UIImage ( cgImage: cgImage)
10611127 self . imageView. image = uiImage
@@ -1201,16 +1267,21 @@ extension CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate, AV
12011267
12021268 let ciImage = CIImage ( cvPixelBuffer: pixelBuffer)
12031269
1204- // Perform person segmentation
1270+ // Perform privacy detection
12051271 let handler = VNImageRequestHandler ( ciImage: ciImage, orientation: . up, options: [ : ] )
12061272
12071273 do {
1208- try handler. perform ( [ personSegmentationRequest] )
1209- if let result = personSegmentationRequest. results? . first as? VNPixelBufferObservation {
1210- self . personMaskBuffer = result. pixelBuffer
1274+ if privacyMode == " segmentation " {
1275+ try handler. perform ( [ personSegmentationRequest] )
1276+ if let result = personSegmentationRequest. results? . first as? VNPixelBufferObservation {
1277+ self . personMaskBuffer = result. pixelBuffer
1278+ }
1279+ } else {
1280+ try handler. perform ( [ bodyPoseRequest] )
1281+ self . detectedBodyPoses = bodyPoseRequest. results ?? [ ]
12111282 }
12121283 } catch {
1213- print ( " Failed to perform person segmentation : \( error) " )
1284+ print ( " Failed to perform privacy detection : \( error) " )
12141285 }
12151286
12161287 // Apply blur to humans
0 commit comments