Skip to content

Instantly share code, notes, and snippets.

@xhruso00
Last active February 27, 2021 21:12
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xhruso00/a3f8a9c8ae7e33b8b23d to your computer and use it in GitHub Desktop.
Save xhruso00/a3f8a9c8ae7e33b8b23d to your computer and use it in GitHub Desktop.
Black & White monochrome 1bit threshold filter. Uses modern 10.15 approach with protocols and metal and half data type. To use simply call [CIFilter blackAndWhiteThresholdFilter]; (add -fcikernel to "Other metal compiler flags" and add -cikernel for User-defined MTTLINKER_FLAGS . Enable fast math in build settings section metal. Targeting earlie…
#include <metal_stdlib>
using namespace metal;
//https://en.wikipedia.org/wiki/List_of_monochrome_and_RGB_palettes
//https://en.wikipedia.org/wiki/Relative_luminance
//https://en.wikipedia.org/wiki/Grayscale
constant float3 kRec709Luma = float3(0.2126, 0.7152, 0.0722);
constant float3 kRec601Luma = float3(0.299 , 0.587 , 0.114);
//constant float3 kRec2100Luma = float3(0.2627, 0.6780, 0.0593);
//universal but doesn't support fast math
#include <CoreImage/CoreImage.h>
extern "C" { namespace coreimage {
float lumin601(float3 p)
{
return dot(p.rgb, kRec601Luma);
}
float lumin709(float3 p)
{
return dot(p.rgb, kRec709Luma);
}
float4 thresholdFilter(sample_t image, float threshold)
{
float4 pix = unpremultiply(image);
float luma = lumin601(pix.rgb);
pix.rgb = float3(step(threshold, luma));
return premultiply(pix);
}
}}
#include <metal_stdlib>
using namespace metal;
//https://en.wikipedia.org/wiki/List_of_monochrome_and_RGB_palettes
//https://en.wikipedia.org/wiki/Relative_luminance
//https://en.wikipedia.org/wiki/Grayscale
//<CoreImage/CIKernelMetalLib.h>
//only if you enable fast math (macOS10.14 or iOS12) otherwise fall back to float4 instead of half4
//forcing compilation for macOS 10.14+//iOS12+
//ideal solution is more metal files and compile into one library
//https://stackoverflow.com/questions/58451581/how-to-compile-two-versions-of-metal-files/58462401#58462401
//#define __CIKERNEL_METAL_VERSION__ 200
constant half3 kRec709Luma = half3(0.2126, 0.7152, 0.0722);
constant half3 kRec601Luma = half3(0.299 , 0.587 , 0.114);
//constant float3 kRec2100Luma = float3(0.2627, 0.6780, 0.0593);
#include <CoreImage/CoreImage.h>
extern "C" { namespace coreimage {
half lumin601(half3 p)
{
return dot(p.rgb, kRec601Luma);
}
half lumin709(half3 p)
{
return dot(p.rgb, kRec709Luma);
}
half4 thresholdFilter(sample_h image, half threshold)
{
half4 pix = unpremultiply(image);
half luma = lumin601(pix.rgb);
pix.rgb = half3(step(threshold, luma));
return premultiply(pix);
}
}}
#import <Cocoa/Cocoa.h>
#import <CoreImage/CoreImage.h>
@protocol BlackAndWhiteThreshold <CIFilter>
@property (nonatomic, retain) CIImage *inputImage;
@property (nonatomic) float threshold;
@end
@interface CIFilter(BlackAndWhiteThresholdFilter)
+ (CIFilter<BlackAndWhiteThreshold>*) blackAndWhiteThresholdFilter;
@property (nonatomic, retain) CIImage *inputImage;
@property (nonatomic) float threshold;
@end
/// 1-bit monochrome filter
@interface BlackAndWhiteThresholdFilter : CIFilter <CIFilter>
//WWDC2014 Session 514 CIFilter subclasses can use @property instead of ivars
@property (nonatomic, retain) NSNumber *inputThreshold;
@property (nonatomic, retain) CIImage *inputImage;
@end
#import "CIFilter+BlackAndWhiteThresholdFilter.h"
#include <objc/runtime.h>
#import <CoreImage/CoreImageDefines.h>
#import "CIColorKernelMetal.h"
#import "CIColorKernelGLSL.h"
@class BlackAndWhiteThresholdFilter;
static NSString *const kCIInputThreshold = @"inputThreshold";
NSString *const kBlackAndWhiteThresholdFilterName = @"BlackAndWhiteThreshold";
static NSString *const kBlackAndWhiteThresholdFilterDisplayName = @"Black & White Threshold";
@implementation CIFilter(BlackAndWhiteThresholdFilter)
@dynamic inputImage;
@dynamic threshold;
+ (CIFilter<BlackAndWhiteThreshold>*) blackAndWhiteThresholdFilter
{
[BlackAndWhiteThresholdFilter class]; //kick off initialize to register filter
CIFilter<BlackAndWhiteThreshold>*filter = (CIFilter<BlackAndWhiteThreshold>*)[CIFilter filterWithName:kBlackAndWhiteThresholdFilterName];
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
/// convenience removing input keyword
class_addMethod([self class], @selector(threshold), (IMP)floatGetter, "f@:");
class_addMethod([self class], @selector(setThreshold:), (IMP)floatSetter, "v@:f");
});
return filter;
}
static float floatGetter(id self, SEL _cmd) {
NSString *selector = NSStringFromSelector(_cmd);
///capitalize first letter
NSString *firstLetter = [[selector substringWithRange:NSMakeRange(0, 1)] uppercaseString];
NSString *key = [@"input" stringByAppendingString:[selector stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstLetter]];
id value = [self valueForKey:key];
float number = NAN;
if (value && [value isKindOfClass:[NSNumber class]]) {
number = [value floatValue];
}
return number;
}
static void floatSetter(id self, SEL _cmd, float value) {
NSString *selector = NSStringFromSelector(_cmd);
NSString *aaa = [selector stringByReplacingCharactersInRange:NSMakeRange(0, 3) withString:@"input"];
[self setValue:@(value) forKey:[aaa substringWithRange:NSMakeRange(0, [aaa length] - 1)]];
}
@end
@interface BlackAndWhiteThresholdFilter()
{
CIColorKernel *_kernel;
}
@end
@implementation BlackAndWhiteThresholdFilter
//more https://developer.apple.com/library/ios/documentation/graphicsimaging/Conceptual/CoreImaging/ci_image_units/ci_image_units.html#//apple_ref/doc/uid/TP30001185-CH7-SW8
+ (void)initialize
{
//verify registration with [CIFilter filterNamesInCategories:@[kCICategoryVideo]]
//registering class responsible for CIFilter execution
[CIFilter registerFilterName:kBlackAndWhiteThresholdFilterName
constructor:(id <CIFilterConstructor>)self //self means class BlackAndWhiteThresholdFilter
classAttributes:@{
kCIAttributeFilterCategories: @[
kCICategoryVideo,
kCICategoryStillImage,
kCICategoryCompositeOperation,
kCICategoryInterlaced,
kCICategoryNonSquarePixels
],
kCIAttributeFilterDisplayName: kBlackAndWhiteThresholdFilterDisplayName,
}];
}
+ (CIFilter *)filterWithName:(NSString *)aName
{
CIColorKernel *kernel;
if (@available(macOS 10.13, *)) {
kernel = [CIColorKernelMetal blackAndWhiteThresholdKernel];
}
if (kernel == nil) {
kernel = [CIColorKernelGLSL blackAndWhiteThresholdKernel];
}
return [[self alloc] initWithKernel:kernel];
}
- (instancetype) initWithKernel:(CIColorKernel *)kernel
{
if (kernel == nil) return nil;
self = [super init];
if (self) {
_kernel = kernel;
}
return self;
}
- (NSArray *)inputKeys {
return @[kCIInputImageKey, kCIInputThreshold];
}
- (NSArray *)outputKeys {
return @[kCIOutputImageKey];
}
// ------------ ------------ ------------ ------------ ------------ ------------
#pragma mark - CIFilter Protocol
+ (NSDictionary *)customAttributes
{
NSDictionary *inputThreshold = @{
kCIAttributeType: kCIAttributeTypeScalar,
kCIAttributeMin: @0.0f,
kCIAttributeMax: @1.0f,
kCIAttributeIdentity : @0.00,
kCIAttributeDefault: @0.5f,
};
return @{
kCIInputThreshold : inputThreshold,
// This is needed because the filter is registered under a different name than the class.
kCIAttributeFilterName : kBlackAndWhiteThresholdFilterName
};
}
- (CIImage *)outputImage {
CIImage *inputImage = [self inputImage];
if ([self inputImage] == nil) {
return nil;
}
CIImage *outputImage;
outputImage = [_kernel applyWithExtent:[inputImage extent]
roiCallback:^CGRect(int index, CGRect destRect) { return destRect; }
arguments:@[inputImage, [self inputThreshold]]];
return outputImage;
}
@end
#import <CoreImage/CoreImage.h>
@interface CIColorKernelGLSL : CIColorKernel
+ (CIColorKernel *)blackAndWhiteThresholdKernel;
- (instancetype)init NS_UNAVAILABLE;
@end
#import "CIColorKernelGLSL.h"
@implementation CIColorKernelGLSL
+ (CIColorKernel *)blackAndWhiteThresholdKernel
{
// WWDC 2017 510 - disadvanage is that this needs to be compiled on first run (performance penalty)
return [self kernelWithString:[self kernelText]];
}
+ (NSString *)kernelText
{
return
@""
"float lumin601(vec3 p)"
"{"
" return dot(p, vec3(0.299 , 0.587 , 0.114));"
"}"
""
"kernel vec4 thresholdFilter(__sample image, float inputThreshold)"
"{"
" vec4 src = unpremultiply( image) );"
" float luma = lumin601( src.rgb );"
" src.rgb = vec3( step( inputThreshold, luma));"
" return premultiply(src);"
"}";
}
//kept for reference purpose
+ (NSString *)oldNonColorKernelText
{
return
@""
"float lumin601(vec3 p)"
"{"
" return dot(p, vec3(0.299 , 0.587 , 0.114));"
"}"
""
"kernel vec4 thresholdFilter(sampler image, float inputThreshold)"
"{"
" vec4 src = unpremultiply( sample(image, samplerCoord(image)) );"
" float luma = lumin601( src.rgb );"
" src.rgb = vec3( step( inputThreshold, luma));"
" return premultiply(src);"
"}";
}
@end
#import <CoreImage/CoreImage.h>
FOUNDATION_EXPORT NSString *const kMetalLibraryOldTarget;
FOUNDATION_EXPORT NSString *const kMetalLibraryFastMathTarget;
NS_AVAILABLE(10_13, 11_0)
@interface CIColorKernelMetal : CIColorKernel
+ (CIColorKernel *)blackAndWhiteThresholdKernel;
- (instancetype)init NS_UNAVAILABLE;
@end
#import "CIColorKernelMetal.h"
#import <Metal/Metal.h>
static NSString *const kMetallibExtension = @"metallib";
NSString *const kMetalLibraryOldTarget = @"Metal_10_13";
NSString *const kMetalLibraryFastMathTarget = @"Metal_10_14";
@implementation CIColorKernelMetal
+ (CIColorKernel *)blackAndWhiteThresholdKernel
{
BOOL supportsMetal;
#if TARGET_OS_IOS
supportsMetal = MTLCreateSystemDefaultDevice() != nil; //this forces GPU on macbook to switch immediatelly
#else
supportsMetal = [MTLCopyAllDevices() count] >= 1;
#endif
//10.13 fully supports metal with fast math, however there are hackintoshes etc...
if (supportsMetal == NO) return nil;
NSError *error;
CIColorKernel *kernel;
kernel = [self kernelWithFunctionName:@"thresholdFilter" fromMetalLibraryData:[self data] error:&error];
if (error) {
NSLog(@"%@", error);
}
return kernel;
}
+ (NSData *)data
{
NSURL *URL = [[NSBundle mainBundle] URLForResource:[self metalLibraryName] withExtension:kMetallibExtension];
NSData *data = [NSData dataWithContentsOfURL:URL];
NSError *error;
if (error) {
NSLog(@"%@", error);
}
return data;
}
+ (NSString *)metalLibraryName
{
if (@available(macOS 10.14, *)) {
return kMetalLibraryFastMathTarget;
} else {
return kMetalLibraryOldTarget;
}
//use default
//return @"default";
}
@end
@klauslanza
Copy link

To make it work in iOS, you should use this edit in the last method.

    CIImage * outputImage = [_kernel applyWithExtent:[inputImage extent]
                                     roiCallback:^CGRect(int index, CGRect destRect) {
                                         return CGRectMake(0, 0, CGRectGetWidth(inputImage.extent), CGRectGetHeight(inputImage.extent));
                                     } arguments:@[ inputImage, inputThreshold `]];

and rename
- (NSDictionary *)customAttributes
to
+ (NSDictionary *)customAttributes

@xhruso00
Copy link
Author

Updated with your suggestions and modernised. Everyone who stumbles here from google or stackoverflow can look at history

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment