-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: replace photo library detection with human face detection #267
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// | ||
// CGImage+Util.swift | ||
// PostHog | ||
// | ||
// Created by Yiannis Josephides on 30/11/2024. | ||
// | ||
|
||
#if os(iOS) | ||
import UIKit | ||
|
||
extension CGImage { | ||
// This class can maintain many state variables that can impact performance. So for best performance, reuse CIDetector instances instead of creating new ones. | ||
static var humanFaceDetector: CIDetector = { | ||
let options = [CIDetectorAccuracy: CIDetectorAccuracyLow] | ||
return CIDetector(ofType: CIDetectorTypeFace, context: nil, options: options)! | ||
}() | ||
|
||
private static var ph_human_face_detected_key: UInt8 = 0 | ||
var ph_human_face_detected: Bool? { | ||
get { | ||
objc_getAssociatedObject(self, &CGImage.ph_human_face_detected_key) as? Bool | ||
} | ||
|
||
set { | ||
objc_setAssociatedObject( | ||
self, | ||
&CGImage.ph_human_face_detected_key, | ||
newValue as Bool?, | ||
.OBJC_ASSOCIATION_RETAIN_NONATOMIC | ||
) | ||
} | ||
} | ||
} | ||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -406,22 +406,41 @@ | |
image.imageAsset?.value(forKey: "_containingBundle") != nil | ||
} | ||
|
||
// Photo library images have a UUID identifier as _assetName (e.g 64EF5A48-2E96-4AB2-A79B-AAB7E9116E3D) | ||
// SF symbol and bundle images have the actual symbol name as _assetName (e.g chevron.backward) | ||
private func isPhotoLibraryImage(_ image: UIImage) -> Bool { | ||
guard config.sessionReplayConfig.maskPhotoLibraryImages else { | ||
return false | ||
private func isAnimatedImageView(_ view: UIImageView) -> Bool { | ||
!(view.animationImages?.isEmpty ?? true) | ||
} | ||
|
||
private func containsHumanFaces(caLayer layer: CALayer) -> Bool { | ||
if let content = layer.contents, CFGetTypeID(content as CFTypeRef) == CGImage.typeID { | ||
// force-casting is safe here because of CFTypeID check above | ||
return containsHumanFaces(cgImage: content as! CGImage) | ||
} | ||
return false | ||
} | ||
|
||
guard let assetName = image.imageAsset?.value(forKey: "_assetName") as? String else { | ||
return false | ||
private func containsHumanFaces(image: UIImage) -> Bool { | ||
guard let cgImage = image.cgImage else { return false } | ||
return containsHumanFaces(cgImage: cgImage) | ||
} | ||
|
||
private func containsHumanFaces(cgImage: CGImage) -> Bool { | ||
let detectAndCache: () -> Bool = { | ||
var faceDetected = false | ||
let ciImage = CIImage(cgImage: cgImage) | ||
faceDetected = !CGImage.humanFaceDetector.features(in: ciImage).isEmpty | ||
cgImage.ph_human_face_detected = faceDetected | ||
return faceDetected | ||
} | ||
|
||
if assetName.isEmpty { return false } | ||
if image.isSymbolImage { return false } | ||
if isAssetsImage(image) { return false } | ||
return cgImage.ph_human_face_detected ?? detectAndCache() | ||
} | ||
|
||
return true | ||
private func isSystemImageView(_ view: UIImageView) -> Bool { | ||
// Came across some system image views like <_UICutoutShadowView> that were masked | ||
// with maskAllImages options (e.g in this case this was a blurred background view of a UIPickerView) | ||
// Let’s be prudent and add a general check on internal classes starting with _UI. | ||
// We may need to be more explicit in the future | ||
String(describing: view).starts(with: "<_UI") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if we should be explicit here for "<_UICutoutShadowView>" but a general test for internal types here seemed like a good idea at the time |
||
} | ||
|
||
private func isAnyInputSensitive(_ view: UIView) -> Bool { | ||
|
@@ -467,9 +486,17 @@ | |
} | ||
|
||
private func isSwiftUIImageSensitive(_ view: UIView) -> Bool { | ||
// No way of checking if this is an asset image or not | ||
// No way of checking if there's actual content in the image or not | ||
config.sessionReplayConfig.maskAllImages || view.isNoCapture() | ||
// sensitive, regardless | ||
if view.isNoCapture() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't refactor |
||
return true | ||
} | ||
|
||
// detect human faces | ||
if config.sessionReplayConfig.maskImagesWithHumanFaces, containsHumanFaces(caLayer: view.layer) { | ||
return true | ||
} | ||
|
||
return config.sessionReplayConfig.maskAllImages | ||
} | ||
|
||
private func isImageViewSensitive(_ view: UIImageView) -> Bool { | ||
|
@@ -481,13 +508,32 @@ | |
return true | ||
} | ||
|
||
if config.sessionReplayConfig.maskAllImages { | ||
// asset images are probably not sensitive | ||
return !isAssetsImage(image) | ||
// system images are likely not sensitive | ||
if isSystemImageView(view) { | ||
return false | ||
} | ||
|
||
// symbol images are probably not sensitive | ||
if image.isSymbolImage { | ||
return false | ||
} | ||
|
||
// an animated image view (e.g spinner view) is most likely not a sensitive asset | ||
if isAnimatedImageView(view) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Came across system loaders in SwiftUI. But could also be gif support through |
||
return false | ||
} | ||
|
||
// try to detect user photo images | ||
return isPhotoLibraryImage(image) | ||
// detect human faces | ||
if config.sessionReplayConfig.maskImagesWithHumanFaces, containsHumanFaces(image: image) { | ||
return true | ||
} | ||
|
||
// asset images are probably not sensitive (unless they contain a face) | ||
if isAssetsImage(image) { | ||
return false | ||
} | ||
|
||
return config.sessionReplayConfig.maskAllImages | ||
} | ||
|
||
private func toWireframe(_ view: UIView) -> RRWireframe? { | ||
|
@@ -662,7 +708,6 @@ | |
private protocol AnyObjectUIHostingViewController: AnyObject {} | ||
|
||
extension UIHostingController: AnyObjectUIHostingViewController {} | ||
|
||
#endif | ||
|
||
// swiftlint:enable cyclomatic_complexity |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,10 +24,10 @@ | |
/// Default: true | ||
@objc public var maskAllSandboxedViews: Bool = true | ||
|
||
/// Enable masking of images that likely originated from user's photo library | ||
/// Experimental support (UIKit only) | ||
/// Enable masking of images that may contain human faces | ||
/// Experimental support | ||
/// Default: true | ||
@objc public var maskPhotoLibraryImages: Bool = true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We cannot do this otherwise we will break people's builds on react native and iOS. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can leave and mark as deprecated for next major release? (and also turn default to false - won't have an impact but just to be clear) |
||
@objc public var maskImagesWithHumanFaces: Bool = true | ||
|
||
/// Enable capturing network telemetry | ||
/// Experimental support | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where we mem-cache the face detection so we avoid detecting multiple times per resource