SNos
SNos

Reputation: 3470

Swift 2.2 - Count Black Pixels in UIImage

I need to count all the black pixels in UIImage. I have found a code that could work however it is written in Objective-C. I have tried to convert it in swift but I get lots of errors and I cannot find the way of fix them.

Whats the best way to do this using Swift?

Simple Imageenter image description here

Objective-C:

/**
 * Structure to keep one pixel in RRRRRRRRGGGGGGGGBBBBBBBBAAAAAAAA format
 */

struct pixel {
    unsigned char r, g, b, a;
};

/**
 * Process the image and return the number of pure red pixels in it.
 */

- (NSUInteger) processImage: (UIImage*) image
{
    NSUInteger numberOfRedPixels = 0;

    // Allocate a buffer big enough to hold all the pixels

    struct pixel* pixels = (struct pixel*) calloc(1, image.size.width * image.size.height * sizeof(struct pixel));
    if (pixels != nil)
    {
        // Create a new bitmap

        CGContextRef context = CGBitmapContextCreate(
            (void*) pixels,
            image.size.width,
            image.size.height,
            8,
            image.size.width * 4,
            CGImageGetColorSpace(image.CGImage),
            kCGImageAlphaPremultipliedLast
        );

        if (context != NULL)
        {
            // Draw the image in the bitmap

            CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, image.size.width, image.size.height), image.CGImage);

            // Now that we have the image drawn in our own buffer, we can loop over the pixels to
            // process it. This simple case simply counts all pixels that have a pure red component.

            // There are probably more efficient and interesting ways to do this. But the important
            // part is that the pixels buffer can be read directly.

            NSUInteger numberOfPixels = image.size.width * image.size.height;

            while (numberOfPixels > 0) {
                if (pixels->r == 255) {
                    numberOfRedPixels++;
                }
                pixels++;
                numberOfPixels--;
            }

            CGContextRelease(context);
        }

        free(pixels);
    }

    return numberOfRedPixels;
}

Upvotes: 3

Views: 1987

Answers (2)

The Beruriah Incident
The Beruriah Incident

Reputation: 3257

Much faster is to use Accelerate's vImageHistogramCalculation to get a histogram of the different channels in your image:

let img: CGImage = CIImage(image: image!)!.cgImage!

let imgProvider: CGDataProvider = img.dataProvider!
let imgBitmapData: CFData = imgProvider.data!
var imgBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: CFDataGetBytePtr(imgBitmapData)), height: vImagePixelCount(img.height), width: vImagePixelCount(img.width), rowBytes: img.bytesPerRow)

let alpha = [UInt](repeating: 0, count: 256)
let red = [UInt](repeating: 0, count: 256)
let green = [UInt](repeating: 0, count: 256)
let blue = [UInt](repeating: 0, count: 256)

let alphaPtr = UnsafeMutablePointer<vImagePixelCount>(mutating: alpha) as UnsafeMutablePointer<vImagePixelCount>?
let redPtr = UnsafeMutablePointer<vImagePixelCount>(mutating: red) as UnsafeMutablePointer<vImagePixelCount>?
let greenPtr = UnsafeMutablePointer<vImagePixelCount>(mutating: green) as UnsafeMutablePointer<vImagePixelCount>?
let bluePtr = UnsafeMutablePointer<vImagePixelCount>(mutating: blue) as UnsafeMutablePointer<vImagePixelCount>?

let rgba = [redPtr, greenPtr, bluePtr, alphaPtr]

let histogram = UnsafeMutablePointer<UnsafeMutablePointer<vImagePixelCount>?>(mutating: rgba)
let error = vImageHistogramCalculation_ARGB8888(&imgBuffer, histogram, UInt32(kvImageNoFlags))

After this runs, alpha, red, green, and blue are now histograms of the colors in your image. If red, green, and blue each only have count in the 0th spot, while alpha only has count in the last spot, your image is black.

If you want to not even check multiple arrays, you can use vImageMatrixMultiply to combine your different channels:

let readableMatrix: [[Int16]] = [
    [3,     0,     0,    0]
    [0,     1,     1,    1],
    [0,     0,     0,    0],
    [0,     0,     0,    0]
]

var matrix: [Int16] = [Int16](repeating: 0, count: 16)

for i in 0...3 {
    for j in 0...3 {
        matrix[(3 - j) * 4 + (3 - i)] = readableMatrix[i][j]
    }
}
vImageMatrixMultiply_ARGB8888(&imgBuffer, &imgBuffer, matrix, 3, nil, nil, UInt32(kvImageNoFlags))

If you stick this in before the histograming, your imgBuffer will be modified in place to average the RGB in each pixel, writing the average out to the B channel. As such, you can just check the blue histogram instead of all three.

(btw, the best description of vImageMatrixMultiply I've found is in the source code, like at https://github.com/phracker/MacOSX-SDKs/blob/2d31dd8bdd670293b59869335d9f1f80ca2075e0/MacOSX10.7.sdk/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/Headers/Transform.h#L21)

Upvotes: 11

CodeBender
CodeBender

Reputation: 36640

I ran into a similar issue now, where I needed to determine if an image was 100% black. The following code will return the number of pure black pixels it finds in an image.

However, if you want to bump the threshold up, you can change the compare value, and allow it to tolerate a wider range of possible colors.

import UIKit

extension UIImage {
    var blackPixelCount: Int {
        var count = 0
        for x in 0..<Int(size.width) {
            for y in 0..<Int(size.height) {
                count = count + (isPixelBlack(CGPoint(x: CGFloat(x), y: CGFloat(y))) ? 1 : 0)
            }
        }

        return count
    }

    private func isPixelBlack(_ point: CGPoint) -> Bool {
        let pixelData = cgImage?.dataProvider?.data
        let pointerData: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

        let pixelInfo = Int(((size.width * point.y) + point.x)) * 4

        let maxValue: CGFloat = 255.0
        let compare: CGFloat = 0.01

        if (CGFloat(pointerData[pixelInfo]) / maxValue) > compare { return false }
        if (CGFloat(pointerData[pixelInfo + 1]) / maxValue) > compare { return false }
        if (CGFloat(pointerData[pixelInfo + 2]) / maxValue) > compare { return false }

        return true
    }
}

You call this with:

let count = image.blackPixelCount

The one caveat is that this is a very slow process, even on small images.

Upvotes: 2

Related Questions