Skip to content

Commit 699d901

Browse files
committed
fix: prefer pixelation over gaussian blur
1 parent aa62688 commit 699d901

4 files changed

Lines changed: 71 additions & 38 deletions

File tree

AllSpark-ios/AppConstants.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ enum AppConstants {
8383
static let actionToggleOff = Color.red
8484
}
8585

86+
enum Privacy {
87+
// High scale for strong privacy masking (pixelation)
88+
static let pixelationScale: CGFloat = 50.0
89+
}
90+
8691
enum Video {
8792
static let defaultChunkDurationMs: Int = 10000
8893
static let dimensionHigh: Int = 1920

AllSpark-ios/CameraViewController.swift

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
6060

6161
// Display layer
6262
private var imageView: UIImageView!
63+
private var loadingOverlay: UIView?
64+
private var activityIndicator: UIActivityIndicatorView?
6365
private var switchCameraButton: UIButton!
6466
private var timerLabel: UILabel!
6567
private var captureModesLabel: UILabel!
@@ -72,6 +74,7 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
7274
super.viewDidLoad()
7375

7476
setupImageView()
77+
setupLoadingOverlay()
7578

7679
setupSwitchCameraButton()
7780
setupTimerLabel()
@@ -124,6 +127,39 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
124127
view.addSubview(imageView)
125128
}
126129

130+
private func setupLoadingOverlay() {
131+
let overlay = UIView(frame: view.bounds)
132+
overlay.backgroundColor = AppConstants.Colors.backgroundBaseUI.withAlphaComponent(AppConstants.UI.overlayOpacityDark)
133+
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
134+
135+
let indicator = UIActivityIndicatorView(style: .large)
136+
indicator.color = .white
137+
indicator.translatesAutoresizingMaskIntoConstraints = false
138+
indicator.startAnimating()
139+
140+
let label = UILabel()
141+
label.text = "Initializing Privacy Models..."
142+
label.textColor = .white
143+
label.font = .systemFont(ofSize: 16, weight: .medium)
144+
label.translatesAutoresizingMaskIntoConstraints = false
145+
146+
let stack = UIStackView(arrangedSubviews: [indicator, label])
147+
stack.axis = .vertical
148+
stack.spacing = AppConstants.UI.spacingMedium
149+
stack.alignment = .center
150+
stack.translatesAutoresizingMaskIntoConstraints = false
151+
152+
overlay.addSubview(stack)
153+
NSLayoutConstraint.activate([
154+
stack.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
155+
stack.centerYAnchor.constraint(equalTo: overlay.centerYAnchor)
156+
])
157+
158+
view.addSubview(overlay)
159+
self.loadingOverlay = overlay
160+
self.activityIndicator = indicator
161+
}
162+
127163
private func setupSwitchCameraButton() {
128164
switchCameraButton = UIButton(type: .system)
129165
switchCameraButton.translatesAutoresizingMaskIntoConstraints = false
@@ -497,13 +533,6 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
497533
let fileExtension = formatString == "mov" ? "mov" : "mp4"
498534
let fileType: AVFileType = formatString == "mov" ? .mov : .mp4
499535

500-
// Get device name for filename
501-
let deviceName = UserDefaults.standard.string(forKey: "deviceName") ?? UIDevice.current.name
502-
let deviceNameForFilename = formatDeviceNameForFilename(deviceName)
503-
504-
// Get camera position for filename
505-
let cameraPosition = currentCameraPosition == .front ? "front" : "back"
506-
507536
let timestampMs = Int(Date().timeIntervalSince1970 * 1000)
508537
let videoName = "tmp_recording_\(timestampMs).\(fileExtension)"
509538
videoURL = documentsPath.appendingPathComponent(videoName)
@@ -976,7 +1005,7 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
9761005
self.privacyMode = UserDefaults.standard.string(forKey: "privacyMode") ?? "segmentation"
9771006

9781007
let segRequest = VNGeneratePersonSegmentationRequest()
979-
segRequest.qualityLevel = .accurate
1008+
segRequest.qualityLevel = .balanced // Balanced is much faster for real-time video than .accurate
9801009
self.personSegmentationRequest = segRequest
9811010

9821011
self.bodyPoseRequest = VNDetectHumanBodyPoseRequest()
@@ -991,10 +1020,10 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
9911020
let scaleY = image.extent.height / maskImage.extent.height
9921021
let scaledMask = maskImage.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))
9931022

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 {
1023+
if let pixelateFilter = CIFilter(name: "CIPixellate") {
1024+
pixelateFilter.setValue(image, forKey: kCIInputImageKey)
1025+
pixelateFilter.setValue(NSNumber(value: Float(AppConstants.Privacy.pixelationScale)), forKey: kCIInputScaleKey)
1026+
if let blurredImage = pixelateFilter.outputImage {
9981027
let clampedBlur = blurredImage.cropped(to: image.extent)
9991028
if let blendFilter = CIFilter(name: "CIBlendWithRedMask") {
10001029
blendFilter.setValue(clampedBlur, forKey: kCIInputImageKey)
@@ -1040,13 +1069,16 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
10401069

10411070
let poseRect = CGRect(x: expandedX, y: expandedY, width: expandedWidth, height: expandedHeight)
10421071

1043-
// Blur the bounding box
1044-
if let blurFilter = CIFilter(name: "CIGaussianBlur") {
1072+
// Pixelate the bounding box
1073+
if let pixelateFilter = CIFilter(name: "CIPixellate") {
10451074
let poseCrop = outputImage.cropped(to: poseRect)
1046-
blurFilter.setValue(poseCrop, forKey: kCIInputImageKey)
1047-
blurFilter.setValue(30.0, forKey: kCIInputRadiusKey)
1075+
pixelateFilter.setValue(poseCrop, forKey: kCIInputImageKey)
1076+
// Set center to the middle of the crop to align blocks locally
1077+
let center = CIVector(x: poseRect.midX, y: poseRect.midY)
1078+
pixelateFilter.setValue(center, forKey: kCIInputCenterKey)
1079+
pixelateFilter.setValue(NSNumber(value: Float(AppConstants.Privacy.pixelationScale)), forKey: kCIInputScaleKey)
10481080

1049-
if let blurredOutput = blurFilter.outputImage {
1081+
if let blurredOutput = pixelateFilter.outputImage {
10501082
let croppedBlur = blurredOutput.cropped(to: poseRect)
10511083
outputImage = croppedBlur.composited(over: outputImage)
10521084
}
@@ -1212,24 +1244,6 @@ class CameraViewController: UIViewController, UINavigationControllerDelegate {
12121244
}
12131245

12141246
// MARK: - Helper Methods
1215-
1216-
private func formatDeviceNameForFilename(_ name: String) -> String {
1217-
// Remove non-ASCII characters
1218-
let ascii = name.filter { $0.isASCII }
1219-
1220-
// Split on non-alphanumeric characters and filter empty components
1221-
let components = ascii.components(separatedBy: CharacterSet.alphanumerics.inverted)
1222-
.filter { !$0.isEmpty }
1223-
1224-
// Build title-case: capitalize first letter of each word
1225-
guard !components.isEmpty else { return "Device" }
1226-
1227-
let result = components.map { word in
1228-
word.prefix(1).uppercased() + word.dropFirst().lowercased()
1229-
}.joined()
1230-
1231-
return result.isEmpty ? "Device" : result
1232-
}
12331247
}
12341248

12351249
extension CameraViewController {
@@ -1296,7 +1310,17 @@ extension CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate, AV
12961310
let uiImage = UIImage(cgImage: cgImage)
12971311

12981312
DispatchQueue.main.async { [weak self] in
1299-
self?.imageView.image = uiImage
1313+
guard let self = self else { return }
1314+
self.imageView.image = uiImage
1315+
1316+
if let overlay = self.loadingOverlay {
1317+
self.loadingOverlay = nil // Nullify immediately to prevent duplicate animations
1318+
UIView.animate(withDuration: 0.3, animations: { [weak overlay] in
1319+
overlay?.alpha = 0
1320+
}) { [weak overlay] _ in
1321+
overlay?.removeFromSuperview()
1322+
}
1323+
}
13001324
}
13011325
}
13021326
}

AllSpark-ios/SettingsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ struct SettingsView: View {
1818
let defaultName = UIDevice.current.name
1919
if UserDefaults.standard.string(forKey: "deviceName") == nil {
2020
UserDefaults.standard.set(defaultName, forKey: "deviceName")
21-
}
21+
}
2222
}
2323

2424
var body: some View {

CONTRIBUTING.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Use `@AppStorage` for managing persistent user preferences and UI toggles.
1515
### 2. Privacy Filtering Patterns
1616

1717
Any new computer vision pipelines must respect user privacy. We utilize Vision framework to automatically identify humans in the frame.
18-
- **Camera Frames:** Real-time facial blurring must be applied to video/image captures before they are transmitted.
18+
- **Camera Frames:** Real-time facial and body blurring prefer pixelation over gaussian blur, and must be applied to video/image captures before they are transmitted.
1919

2020
### 3. Code Style & Architecture
2121

@@ -26,6 +26,10 @@ Any new computer vision pipelines must respect user privacy. We utilize Vision f
2626

2727
**All dependencies must use exact, pegged versions** (no `^`, `~`, or `*` ranges). This prevents version drift across environments and ensures reproducible builds for security.
2828

29+
### 5. Magic Numbers & Constants
30+
31+
**No magic numbers allowed inline.** Any numerical layout properties, structural modifiers (like opacities, heights, constraints), and complex configurations (duration bounds, bitrates) must be formally extracted and organized into the `AppConstants.swift` structure. This guarantees centralized governance of our UI aesthetics and networking policies.
32+
2933
## Build & Test
3034

3135
To build the project locally, open `AllSpark-ios.xcodeproj` with Xcode.

0 commit comments

Comments
 (0)