Last active
October 11, 2023 02:46
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* ordered_dither.m | |
* | |
* Description: Convert an image to a black and white image and use ordered dithering | |
* Author: Chad Armstrong ([email protected]) | |
* 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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example ordered dither on the peppers.tiff image: