-1

I'm trying to apply this mask to the original image

enter image description here

Notice there are some grey areas, I want to keep everything except PURE black.

I've found this code

 func maskImage(image: UIImage, mask: UIImage) -> UIImage {
    let imageReference = (image.cgImage)!
    let maskReference = (mask.cgImage)!

    let imageMask = CGImage(
        maskWidth: maskReference.width
        , height: maskReference.height
        , bitsPerComponent: maskReference.bitsPerComponent
        , bitsPerPixel: maskReference.bitsPerPixel
        , bytesPerRow: maskReference.bytesPerRow
        , provider: maskReference.dataProvider!
        , decode: nil
        , shouldInterpolate: true
    )

    return (UIImage(cgImage: (imageReference.masking(imageMask!))!))
}

But it does the opposite, it removes all white pixels instead.

Edit ---

For example if the above mask is implied to a pure color background

enter image description here

right now so far the solution submitted gives this image

enter image description here

Notice there are holes in the middle, grey areas. Everything should be solid blue. In order words, the mask should only take out PURE black areas.

erotsppa
  • 14,248
  • 33
  • 123
  • 181
  • It would be very helpful if you can add 3 states of a sample image to the post: 1. Before being masked, 2. Current masked, 3. Desired masked – Mojtaba Hosseini Jul 03 '23 at 04:24
  • @MojtabaHosseini I have added all the details now – erotsppa Jul 03 '23 at 04:40
  • You’ve missed the state 3. The desired masked image – Mojtaba Hosseini Jul 03 '23 at 04:55
  • @MojtabaHosseini desired mask image is flexible, as long as it produces a solid blue final image. But I'd imagine the correct mask image would be to (1) turn all black pixels into white and (2) turn all pixels that are NOT PURE BLACK into black – erotsppa Jul 03 '23 at 05:57

2 Answers2

1

Update the mask before applying it like:

let maskReference = (mask.turnedNotPureBlackIntoBlack()!.cgImage)!

Using this extension:

extension UIImage {
    func turnedNotPureBlackIntoBlack() -> UIImage? {
        guard let cgImage else { return nil }

        let width = cgImage.width
        let height = cgImage.height

        let bytesPerPixel = 4
        let bitsPerComponent = 8

        let bytesPerRow = bytesPerPixel * width
        let pixels = UnsafeMutablePointer<UInt32>.allocate(capacity: height * width)

        guard let context = CGContext(
            data: pixels,
            width: width,
            height: height,
            bitsPerComponent: bitsPerComponent,
            bytesPerRow: bytesPerRow,
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
        ) else {
            return nil
        }

        context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))

        for j in 0..<height {
            for i in 0..<width {
                let currentPixel = pixels + j * width + i
                let color = currentPixel.pointee
                let r = UInt32((color >> 00) & 0xFF)
                let g = UInt32((color >> 08) & 0xFF)
                let b = UInt32((color >> 16) & 0xFF)
                let a = UInt32((color >> 24) & 0xFF)

                let pureBlack = 0
                switch (r, g, b) {
                case (0, 0, 0): currentPixel.pointee = 0xFFFFFFFF
                default: currentPixel.pointee = UInt32(pureBlack)
                }
            }
        }

        guard let newCGImage = context.makeImage() else { return nil }

        let processedImage = UIImage(cgImage: newCGImage)
        pixels.deallocate()

        return processedImage
    }
}
Demo:
Original Mask Converted Mask
Original Converted

Note that some pixels in the original image are NOT pure black but its not easy to see them with plain eyes

By the way, I suggest inverting the mask outside the code for better performance if possible

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • This solution is close. But can you explain the blackground part? I need to keep everything except pure black. And you cant apply a black background to the inverted mask. Can you update your code to produce result so I can accept answer? – erotsppa Jul 03 '23 at 01:26
  • I dont want a black background, I need the mask to take all the grey parts except pure black – erotsppa Jul 03 '23 at 04:27
  • Ok, now you can turn all pixels that are NOT PURE BLACK into black. Also, no need for inverting anymore since I've merged that logic into this one – Mojtaba Hosseini Jul 03 '23 at 08:11
  • excellent, I've accepted both answer. Can I ask for a bonus? After the masking is done, it appears that it might be a good idea to trim 1 px on the edge as maybe some of the grey pixels on the edge of the subject turned black. Is there a way to do this? – erotsppa Jul 03 '23 at 14:52
  • It's possible by changing some of the pixles but requires a bit of logic. If you played around with parameters and didn't find out yourself, feel free to open another question so everyone can help. You can link it here so I can take a look too but Im sure you can do it ;) – Mojtaba Hosseini Jul 03 '23 at 17:11
  • It turns out that some pixels inside the mark is not only grey but entirely holes. Is there a way to fill in the holes. Question posted here https://stackoverflow.com/questions/76624120/how-to-trim-all-transparent-pixels-outside-the-subject-but-not-inside-in-swift – erotsppa Jul 05 '23 at 21:23
0

func maskImage(image: UIImage, mask: UIImage) -> UIImage {
    let imageReference = image.cgImage!
    let maskReference = mask.cgImage!
    
    let width = maskReference.width
    let height = maskReference.height
    let bitsPerComponent = maskReference.bitsPerComponent
    let bitsPerPixel = maskReference.bitsPerPixel
    let bytesPerRow = maskReference.bytesPerRow
    
    let context = CGContext(
        data: nil,
        width: width,
        height: height,
        bitsPerComponent: bitsPerComponent,
        bytesPerRow: bytesPerRow,
        space: CGColorSpaceCreateDeviceGray(),
        bitmapInfo: CGImageAlphaInfo.none.rawValue
    )
    
    context!.draw(maskReference, in: CGRect(x: 0, y: 0, width: width, height: height))
    
    let maskImage = context!.makeImage()!
    
    let maskedReference = imageReference.masking(maskImage)!
    
    return UIImage(cgImage: maskedReference)
}

This code creates a new CGContext specifically for the mask image and sets the color space to grayscale (CGColorSpaceCreateDeviceGray()) to ensure that only black pixels are treated as opaque. The CGImageAlphaInfo.none parameter is used to specify that the mask image has no alpha channel.

By drawing the maskReference into the context, the mask is created with the desired behavior of keeping all non-black pixels. Then, the maskImage is used to mask the imageReference, resulting in an image with the desired effect.

Sudeep P H
  • 244
  • 5
  • For some reason your code doesn't work with pngData. The return UIImage renders fine inside SwiftUI. But when you call pngData, you get the full image without any mask applied – erotsppa Jun 30 '23 at 21:26