Created
September 10, 2026 18:56
-
-
Save zats/6332c0c9b75e861ea1f384a70ba9531c to your computer and use it in GitHub Desktop.
Using private CIInpaintingFilter on macOS to recreate Photos "Clean up" feature
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import Foundation | |
| import CoreImage | |
| struct CleanupError: LocalizedError { | |
| let message: String | |
| var errorDescription: String? { message } | |
| } | |
| /// Remove a rectangle using the OS inpainting model. Rect uses whole pixels from the top-left of the oriented image. | |
| public func cleanUp(_ image: CIImage, rect: CGRect) throws -> CIImage { | |
| let source = image.transformed(by: .init(translationX: -image.extent.minX, y: -image.extent.minY)) | |
| guard rect.width > 0, rect.height > 0, rect == rect.integral, source.extent.contains(rect) else { | |
| throw CleanupError(message: "The rectangle must use whole pixels and fit inside the image.") | |
| } | |
| guard let filter = CIFilter(name: "CIInpaintingFilter") else { | |
| throw CleanupError(message: "This OS does not provide the inpainting filter.") | |
| } | |
| let area = CGRect(x: rect.minX, y: source.extent.height - rect.maxY, width: rect.width, height: rect.height) | |
| let mask = CIImage(color: .white).cropped(to: area).composited(over: CIImage(color: .black).cropped(to: source.extent)) | |
| filter.setValue(source, forKey: kCIInputImageKey) | |
| // The inpainting filter removes black pixels; the blend mask uses white. | |
| filter.setValue(mask.applyingFilter("CIColorInvert"), forKey: kCIInputMaskImageKey) | |
| filter.setValue(CIVector(cgRect: area), forKey: "inputMaskBoundingBox") | |
| guard let generated = filter.outputImage else { | |
| throw CleanupError(message: "The inpainting filter did not return an image.") | |
| } | |
| return generated.applyingFilter("CIBlendWithMask", parameters: [kCIInputBackgroundImageKey: source, kCIInputMaskImageKey: mask]).cropped(to: source.extent) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import Foundation | |
| import CoreImage | |
| do { | |
| let args = CommandLine.arguments | |
| guard args.count == 6 || args.count == 7 else { | |
| throw CleanupError(message: "Usage: cleanup-image INPUT X Y WIDTH HEIGHT [OUTPUT.png]") | |
| } | |
| let values = args[2...5].compactMap(Int.init) | |
| guard values.count == 4 else { | |
| throw CleanupError(message: "X, Y, WIDTH and HEIGHT must be integers in pixels.") | |
| } | |
| let inputURL = URL(fileURLWithPath: args[1]) | |
| let outputURL = args.count == 7 ? URL(fileURLWithPath: args[6]) : inputURL.deletingPathExtension().appendingPathExtension("cleaned.png") | |
| guard !FileManager.default.fileExists(atPath: outputURL.path) else { | |
| throw CleanupError(message: "The output already exists. Choose a new output path.") | |
| } | |
| guard let source = CIImage(contentsOf: inputURL, options: [.applyOrientationProperty: true]) else { | |
| throw CleanupError(message: "Core Image cannot read the input image.") | |
| } | |
| let rect = CGRect(x: values[0], y: values[1], width: values[2], height: values[3]) | |
| let result = try cleanUp(source, rect: rect) | |
| try CIContext().writePNGRepresentation(of: result, to: outputURL, format: .RGBA8, colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!) | |
| print(outputURL.path) | |
| } catch { | |
| FileHandle.standardError.write(Data("\(error.localizedDescription)\n".utf8)) | |
| exit(1) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment