Skip to content

Instantly share code, notes, and snippets.

@edenwaith
Last active October 11, 2023 02:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save edenwaith/007a65fcedbd4f717a397b288043ad3c to your computer and use it in GitHub Desktop.
Save edenwaith/007a65fcedbd4f717a397b288043ad3c to your computer and use it in GitHub Desktop.
Convert an image to a black and white image and use ordered dithering
/*
* ordered_dither.m
*
* Description: Convert an image to a black and white image and use ordered dithering
* Author: Chad Armstrong (chad@edenwaith.com)
* Date: 4-5 September 2023
* To compile: gcc -w -framework Foundation -framework AppKit -framework QuartzCore ordered_dither.m -o ordered_dither
* To run: ./ordered_dither path/to/image.png [dither level]
*
*/
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h> // Used for images
#import <QuartzCore/CIFilter.h> // Used for CIFilter
// Prototypes
CGFloat ditherValue(int ditherLevel, int col, int row);
// Bayer Dithering Matricies
int dm2x2[2][2] = {
{ 0, 3, },
{ 2, 1 }
};
int dm2x2_alt_1[2][2] = {
{ 0, 2, },
{ 3, 1 }
};
int dm2x2_alt_2[2][2] = {
{ 3, 1, },
{ 0, 2 }
};
const int dither_matrix_2x2[2][2] = {
{ 0, 255 },
{ 170, 85 }
};
float bayer_pattern_2x2[2][2] = {
{ 0.2, 0.8 },
{ 0.6, 0.4 }
};
float varied_bayer_pattern_2x2[2][2] = {
{ 0.3, 0.45 },
{ 0.55, 0.7 }
};
int dm4x4[4][4] = {
{ 0, 12, 3, 15, },
{ 8, 4, 11, 7, },
{ 2, 14, 1, 13, },
{ 10, 6, 9, 5 }
};
int magic_square[4][4] = {
{ 0, 6, 9, 15 },
{ 11, 13, 2, 4 },
{ 7, 1, 14, 8 },
{ 12, 10, 5, 3 }
};
int dm8x8[8][8] = {
{ 0, 48, 12, 60, 3, 51, 15, 63, },
{ 32, 16, 44, 28, 35, 19, 47, 31, },
{ 8, 56, 4, 52, 11, 59, 7, 55, },
{ 40, 24, 36, 20, 43, 27, 39, 23, },
{ 2, 50, 14, 62, 1, 49, 13, 61, },
{ 34, 18, 46, 30, 33, 17, 45, 29, },
{ 10, 58, 6, 54, 9, 57, 5, 53, },
{ 42, 26, 38, 22, 41, 25, 37, 21 }
};
int dm16x16[16][16] = {
{ 0, 192, 48, 240, 12, 204, 60, 252, 3, 195, 51, 243, 15, 207, 63, 255 },
{ 128, 64, 176, 112, 140, 76, 188, 124, 131, 67, 179, 115, 143, 79, 191, 127 },
{ 32, 224, 16, 208, 44, 236, 28, 220, 35, 227, 19, 211, 47, 239, 31, 223 },
{ 160, 96, 144, 80, 172, 108, 156, 92, 163, 99, 147, 83, 175, 111, 159, 95 },
{ 8, 200, 56, 248, 4, 196, 52, 244, 11, 203, 59, 251, 7, 199, 55, 247 },
{ 136, 72, 184, 120, 132, 68, 180, 116, 139, 75, 187, 123, 135, 71, 183, 119 },
{ 40, 232, 24, 216, 36, 228, 20, 212, 43, 235, 27, 219, 39, 231, 23, 215 },
{ 168, 104, 152, 88, 164, 100, 148, 84, 171, 107, 155, 91, 167, 103, 151, 87 },
{ 2, 194, 50, 242, 14, 206, 62, 254, 1, 193, 49, 241, 13, 205, 61, 253 },
{ 130, 66, 178, 114, 142, 78, 190, 126, 129, 65, 177, 113, 141, 77, 189, 125 },
{ 34, 226, 18, 210, 46, 238, 30, 222, 33, 225, 17, 209, 45, 237, 29, 221 },
{ 162, 98, 146, 82, 174, 110, 158, 94, 161, 97, 145, 81, 173, 109, 157, 93 },
{ 10, 202, 58, 250, 6, 198, 54, 246, 9, 201, 57, 249, 5, 197, 53, 245 },
{ 138, 74, 186, 122, 134, 70, 182, 118, 137, 73, 185, 121, 133, 69, 181, 117 },
{ 42, 234, 26, 218, 38, 230, 22, 214, 41, 233, 25, 217, 37, 229, 21, 213 },
{ 170, 106, 154, 90, 166, 102, 150, 86, 169, 105, 153, 89, 165, 101, 149, 85 }
};
/////////////////////////////////////////////////////////////////////////////
// Ordered dither matrices
/////////////////////////////////////////////////////////////////////////////
// Reference: https://www.codeproject.com/Articles/5259216/Dither-Ordered-and-Floyd-Steinberg-Monochrome-Colo
const int BAYER_PATTERN_2X2[2][2] = { // 2x2 Bayer Dithering Matrix. Color levels: 5
{ 51, 206 },
{ 153, 102 }
};
const int BAYER_PATTERN_3X3[3][3] = { // 3x3 Bayer Dithering Matrix. Color levels: 10
{ 181, 231, 131 },
{ 50, 25, 100 },
{ 156, 75, 206 }
};
const int BAYER_PATTERN_4X4[4][4] = { // 4x4 Bayer Dithering Matrix. Color levels: 17
{ 15, 195, 60, 240 },
{ 135, 75, 180, 120 },
{ 45, 225, 30, 210 },
{ 165, 105, 150, 90 }
};
// Unlike with the previous three patterns, the first number starts at 0, which causes a
// problem with large patches of black and it creates specks of white in the slot_machine image
// Also with this particular pattern, the largest number is in the bottom left instead of the
// top right.
const int BAYER_PATTERN_8X8[8][8] = { // 8x8 Bayer Dithering Matrix. Color levels: 65
{ 0, 128, 32, 160, 8, 136, 40, 168 },
{ 192, 64, 224, 96, 200, 72, 232, 104 },
{ 48, 176, 16, 144, 56, 184, 24, 152 },
{ 240, 112, 208, 80, 248, 120, 216, 88 },
{ 12, 140, 44, 172, 4, 132, 36, 164 },
{ 204, 76, 236, 108, 196, 68, 228, 100 },
{ 60, 188, 28, 156, 52, 180, 20, 148 },
{ 252, 124, 220, 92, 244, 116, 212, 84 }
};
const int BAYER_PATTERN_16X16[16][16] = { // 16x16 Bayer Dithering Matrix. Color levels: 256
{ 0, 191, 48, 239, 12, 203, 60, 251, 3, 194, 51, 242, 15, 206, 63, 254 },
{ 127, 64, 175, 112, 139, 76, 187, 124, 130, 67, 178, 115, 142, 79, 190, 127 },
{ 32, 223, 16, 207, 44, 235, 28, 219, 35, 226, 19, 210, 47, 238, 31, 222 },
{ 159, 96, 143, 80, 171, 108, 155, 92, 162, 99, 146, 83, 174, 111, 158, 95 },
{ 8, 199, 56, 247, 4, 195, 52, 243, 11, 202, 59, 250, 7, 198, 55, 246 },
{ 135, 72, 183, 120, 131, 68, 179, 116, 138, 75, 186, 123, 134, 71, 182, 119 },
{ 40, 231, 24, 215, 36, 227, 20, 211, 43, 234, 27, 218, 39, 230, 23, 214 },
{ 167, 104, 151, 88, 163, 100, 147, 84, 170, 107, 154, 91, 166, 103, 150, 87 },
{ 2, 193, 50, 241, 14, 205, 62, 253, 1, 192, 49, 240, 13, 204, 61, 252 },
{ 129, 66, 177, 114, 141, 78, 189, 126, 128, 65, 176, 113, 140, 77, 188, 125 },
{ 34, 225, 18, 209, 46, 237, 30, 221, 33, 224, 17, 208, 45, 236, 29, 220 },
{ 161, 98, 145, 82, 173, 110, 157, 94, 160, 97, 144, 81, 172, 109, 156, 93 },
{ 10, 201, 58, 249, 6, 197, 54, 245, 9, 200, 57, 248, 5, 196, 53, 244 },
{ 137, 74, 185, 122, 133, 70, 181, 118, 136, 73, 184, 121, 132, 69, 180, 117 },
{ 42, 233, 26, 217, 38, 229, 22, 213, 41, 232, 25, 216, 37, 228, 21, 212 },
{ 169, 106, 153, 90, 165, 102, 149, 86, 168, 105, 152, 89, 164, 101, 148, 85 }
};
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Specify the path to the image
if (argc < 2) {
printf("usage: %s path/to/image [dither_level]\n", argv[0]);
exit(EXIT_FAILURE);
}
// Get the specified image path
NSString *imagePath = [NSString stringWithUTF8String:argv[1]];
int ditherLevel = 16;
if (argc == 3) {
ditherLevel = atoi(argv[2]);
}
// Import into an NSData object or NSImage
NSImage *originalImage = [[NSImage alloc] initWithContentsOfFile: imagePath];
if (originalImage == NULL)
{
NSLog(@"The image failed to load");
return EXIT_FAILURE;
}
NSSize originalImageSize = [originalImage size];
double aspectRatio = 1.0;
CGFloat newImageHeight = originalImageSize.height;
CGFloat newImageWidth = originalImageSize.width;
NSBitmapImageRep *bitmap = [[originalImage representations] objectAtIndex: 0];
int col = 0;
int row = 0;
int modVal = ditherLevel;
// Cycle through each pixel and find the nearest color and replace it in the standard EGA palette
for (int y = 0; y < (int)originalImageSize.height; y++)
{
row = y % modVal; // y & 15 == y % 16
for (int x = 0; x < (int)originalImageSize.width; x++)
{
col = x % modVal; // x & 15 == x % 16
NSColor *originalPixelColor = [bitmap colorAtX:x y:y];
CGFloat red = [originalPixelColor redComponent];
CGFloat green = [originalPixelColor greenComponent];
CGFloat blue = [originalPixelColor blueComponent];
// Divide by 255.0 to normalize the value
CGFloat bayerDitherValue = ditherValue(ditherLevel, col, row);
CGFloat avgPixelColor = (red + green + blue)/3.0;
CGFloat newColor = 0.0;
// For some of the matrices (8x8 and 16x16), if the avgPixelColor is full black or white might be
// incorrectly calculated.
if (avgPixelColor == 0.0 || avgPixelColor == 1.0) {
newColor = avgPixelColor;
} else {
newColor = avgPixelColor < bayerDitherValue ? 0 : 1.0;
}
NSColor *newPixelColor = [NSColor colorWithCalibratedRed: newColor green: newColor blue: newColor alpha: 1.0];
[bitmap setColor: newPixelColor atX: x y: y];
}
}
// Convert bitmap back into an NSImage
NSImage *oneBitDitheredImage = [[NSImage alloc] initWithSize:[bitmap size]];
[oneBitDitheredImage addRepresentation: bitmap];
// Save NSBitmapImageRep to an image and save to disk
NSString *fileName = [imagePath stringByDeletingPathExtension];
NSString *oneBitDitheredImagePath = [NSString stringWithFormat:@"%@_dithered_%dx%d.png", fileName, modVal, modVal];
NSBitmapImageRep *imgRep = [[oneBitDitheredImage representations] objectAtIndex: 0];
NSData *data = [imgRep representationUsingType: NSPNGFileType properties: nil];
[data writeToFile: oneBitDitheredImagePath atomically: NO];
[pool release];
return 0;
}
CGFloat ditherValue(int ditherLevel, int col, int row) {
if (ditherValue == 2) {
// Other 2x2 matrix patterns to try
// return dm2x2[col][row] / 3.0;
// return bayer_pattern_2x2[col][row];
// return varied_bayer_pattern_2x2[col][row];
return BAYER_PATTERN_2X2[col][row] / 255.0;
} else if (ditherValue == 3) {
return BAYER_PATTERN_3X3[col][row] / 255.0;
} else if (ditherValue == 4) {
return magic_square[col][row] / 255.0;
// return BAYER_PATTERN_4X4[col][row] / 255.0;
} else if (ditherValue == 8) {
return BAYER_PATTERN_8X8[col][row] / 255.0;
} else {
return BAYER_PATTERN_16X16[col][row] / 255.0;
}
}
@edenwaith
Copy link
Author

Example ordered dither on the peppers.tiff image:
peppers_dithered_16x16

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