Skip to content

Commit

Permalink
Rewrote sticker detection (#2546)
Browse files Browse the repository at this point in the history
* rewrote alpha check logic

* Update UIImage+Extensions.swift

Co-authored-by: Nathan Mattes <[email protected]>

---------

Co-authored-by: Nathan Mattes <[email protected]>
  • Loading branch information
Amzd and zeitschlag authored Jan 24, 2025
1 parent 7cf10a3 commit f020eae
Showing 1 changed file with 41 additions and 20 deletions.
61 changes: 41 additions & 20 deletions DcCore/DcCore/Extensions/UIImage+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,30 +64,51 @@ public extension UIImage {
if !self.isTransparent() {
return false
}
guard let cgImage = self.cgImage,
let data = cgImage.dataProvider?.data as Data?,
let dataPtr = data.withUnsafeBytes({ $0.bindMemory(to: UInt8.self).baseAddress }),
!data.isEmpty else {
return false // Unable to get the CGImage or image data
}

let width = cgImage.width
let height = cgImage.height
let bytesPerRow = cgImage.bytesPerRow
guard let cgImage else { return false }

// Check the alpha values of the pixels at the corners
let topLeftIndex = 0
let topRightIndex = max(0, width - 1)
let bottomLeftIndex = max(0, bytesPerRow * (height - 1))
let bottomRightIndex = max(0, bytesPerRow * (height - 1) + width - 1)
// https://christianselig.com/2021/04/efficient-average-color/
// First, resize the image. We do this for two reasons, 1) less pixels to deal with means faster calculation and a resized image still has the "gist" of the colors, and 2) the image we're dealing with may come in any of a variety of color formats (CMYK, ARGB, RGBA, etc.) which complicates things, and redrawing it normalizes that into a base color format we can deal with.
// 40x40 is a good size to resize to still preserve quite a bit of detail but not have too many pixels to deal with. Aspect ratio is irrelevant for just finding average color.
let size = CGSize(width: 40, height: 40)

if dataPtr[topLeftIndex] < 255,
dataPtr[topRightIndex] < 255,
dataPtr[bottomLeftIndex] < 255,
dataPtr[min(data.count - 1, bottomRightIndex)] < 255 {
return true
let width = Int(size.width)
let height = Int(size.height)
let totalPixels = width * height

let colorSpace = CGColorSpaceCreateDeviceRGB()

// ARGB format
let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue

// 8 bits for each color channel, we're doing ARGB so 32 bits (4 bytes) total, and thus if the image is n pixels wide, and has 4 bytes per pixel, the total bytes per row is 4n. That gives us 2^8 = 256 color variations for each RGB channel or 256 * 256 * 256 = ~16.7M color options in total. That seems like a lot, but lots of HDR movies are in 10 bit, which is (2^10)^3 = 1 billion color options!
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo) else { return false }

// Draw our resized image
context.draw(cgImage, in: CGRect(origin: .zero, size: size))

guard let pixelBuffer = context.data else { return false }

// Bind the pixel buffer's memory location to a pointer we can use/access
let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height)

// Keep track of total fully transparent pixes
var totalFullyTransparentPixels = 0

for x in 0 ..< width {
for y in 0 ..< height {
let pixel = pointer[(y * width) + x]
if alpha(for: pixel) == 0 {
totalFullyTransparentPixels += 1
}
}
}

return false
// More than 10% transparent, probably a sticker
return CGFloat(totalFullyTransparentPixels) / CGFloat(totalPixels) > 0.1
}

private func alpha(for pixel: UInt32) -> UInt8 {
return UInt8((pixel >> 24) & 255)
}
}

0 comments on commit f020eae

Please sign in to comment.