/* Code by lingib Last update 10 December 2020 ---------- Theory ---------- - An RGB color image is separated into its Cyan, Magenta, and Yellow (CMY) components - Each of these CMY components, and the original RGB image, are converted to a grayscale image then dithered - The adjacent dots in each of these these dithered images are then connected using a recursive algorithm. - A low resolution color image is obtained if each of the these images are overlayed and plotted using an appropriate CMYK pen ---------- Notes ---------- - All globals start with an underscore - Apart from the Floyd-Steinberg dither all algorithms are mine. - The formulas for converting from RGB to CMYK are found in serveral places on the net. ---------- Copyright ---------- This code is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License. If not, see . */ // ------------------------ // declarations // ------------------------ PrintWriter monoOutput; //instantiate mono output PrintWriter cyanOutput; //instantiate cyan output PrintWriter magentaOutput; //instantiate magenta output PrintWriter yellowOutput; //instantiate yellow output PImage _source; // source image PImage _mono; // small _mono image PImage _cyan; // small cyan image PImage _magenta; // small magenta image PImage _yellow; // small yellowimage int _resolution = 4; // controls line spacing (1 = highest resolution)) float _gcodeScaleFactor = 0.338; // scales gcode file up/down (printed imageSize = pixels*_gcodeScalFactor) int _width; // small image width = _source.width / _resolution int _height; // small image height = _sourc.height /_resolution // ----- trace() parameters boolean recursionFlag; //3x3 matrix scan boolean lastCommandG00 = false; int lastX = 0; int lastY = 0; int recursionDepth; //maximum recursion recursionDepth ... stops stack overflow // ------------------------ // setup() // ------------------------ void setup() { // ----- screen dimensions size(1024, 768); // screen dimensions for two side-by-side 512x768 pixel images background(255); // background color // ----- get source image _source = loadImage("flower.jpg"); // get color image image(_source, _source.width, 0); // ----- calculate small image dimensions _width = _source.width/_resolution; _height = _source.height/_resolution; // ----- create small workspaces _mono = createImage(_width, _height, RGB); // create _mono workspace _cyan = createImage(_width, _height, RGB); // create cyan workspace _magenta = createImage(_width, _height, RGB); // create magenta workspace _yellow = createImage(_width, _height, RGB); // create yellow workspace // ----- change workspace backgrounds from black to white _mono.filter(INVERT); _cyan.filter(INVERT); _magenta.filter(INVERT); _yellow.filter(INVERT); // ----- create small images pixelate(_source); _source.loadPixels(); _mono.loadPixels(); _cyan.loadPixels(); _magenta.loadPixels(); _yellow.loadPixels(); // ----- create small images for (int x = 0; x < _width; x++) { for (int y = 0; y <_height; y++) { int pixel1 = (x * _resolution) + (y *_resolution* _source.width); // source pixel int pixel2 = x + y * _width; // workspace pixel _mono.pixels[pixel2] = _source.pixels[pixel1]; _cyan.pixels[pixel2] = _source.pixels[pixel1]; _magenta.pixels[pixel2] = _source.pixels[pixel1]; _yellow.pixels[pixel2] = _source.pixels[pixel1]; } } // ----- create rgb, cyan, magenta, and yellow images for (int x = 0; x < _width; x++) { for (int y = 0; y < _height; y++) { color c = _mono.pixels[x + y *_width]; float r = red(c); float g = green(c); float b = blue(c); // https://www.rapidtables.com/convert/color/rgb-to-cmyk.html // R = r/255; // G = g/255; // B = b/255; // K = 1-max(R,max(G,B)); // C = (1-R-K)/1-K) // C = (1-G-K)/1-K) // C = (1-B-K)/1-K) float R = r/255; float G = g/255; float B = b/255; float K = 1-max(R, max(G, B)); float C = (1-R-K)/(1-K); float M = (1-G-K)/(1-K); float Y = (1-B-K)/(1-K); _mono.pixels[x + y *_width] = color(r, g, b); // mono = red + green + blue _cyan.pixels[x + y *_width] = color((1-C)*255); // (1-C) inverts level _magenta.pixels[x + y *_width] = color((1-M)*255); // (1-M) inverts level _yellow.pixels[x + y *_width] = color((1-Y)*255); // (1-Y) inverts level } } _yellow.updatePixels(); _magenta.updatePixels(); _cyan.updatePixels(); _mono.updatePixels(); // ----- convert images to grayscale _mono.filter(GRAY); _cyan.filter(GRAY); _magenta.filter(GRAY); _yellow.filter(GRAY); // ----- dither (stipple) images dither(_mono); dither(_cyan); dither(_magenta); dither(_yellow); //// ----- display images //image(_mono, _source.width, 0); //image(_cyan, _source.width, _source.height - _height); //image(_magenta, _source.width + _width, _source.height - _height); //image(_yellow, _source.width + 2 * _width, _source.height - _height); // ----- Create a new file in the sketch directory monoOutput = createWriter("mono.ngc"); cyanOutput = createWriter("cyan.ngc"); magentaOutput = createWriter("magenta.ngc"); yellowOutput = createWriter("yellow.ngc"); noLoop(); //main loop only runs once } // ------------------------ // draw() // ------------------------ void draw() { strokeWeight(4); // -----trace RGB image _mono.loadPixels(); for (int y=1; y<_mono.height-1; y++) { for (int x=1; x<_mono.width-1; x++) { recursionFlag = true; recursionDepth = 1000; traceMono(x, y); } } _mono.updatePixels(); // ----- trace cyan image _cyan.loadPixels(); for (int y = 1; y< _cyan.height-1; y++) { for (int x=1; x< _cyan.width-1; x++) { recursionFlag = true; recursionDepth = 1000; traceCyan(x, y); } } _cyan.updatePixels(); // ----- trace magenta image _magenta.loadPixels(); for (int y = 1; y< _magenta.height-1; y++) { for (int x=1; x< _magenta.width-1; x++) { recursionFlag = true; recursionDepth = 1000; traceMagenta(x, y); } } _magenta.updatePixels(); // ----- trace yellow image _yellow.loadPixels(); for (int y = 1; y< _yellow.height-1; y++) { for (int x=1; x< _yellow.width-1; x++) { recursionFlag = true; recursionDepth = 1000; traceYellow(x, y); } } _yellow.updatePixels(); // ----- close the mono file monoOutput.println("G00 X0 Y0"); //home monoOutput.flush(); //writes the remaining data to the file monoOutput.close(); //finishes the file // ----- close the cyan file cyanOutput.println("G00 X0 Y0"); //home cyanOutput.flush(); //writes the remaining data to the file cyanOutput.close(); //finishes the file // ----- close the magenta file magentaOutput.println("G00 X0 Y0"); //home magentaOutput.flush(); //writes the remaining data to the file magentaOutput.close(); //finishes the file // ----- close the yellow file yellowOutput.println("G00 X0 Y0"); //home yellowOutput.flush(); //writes the remaining data to the file yellowOutput.close(); //finishes the file } // ------------------------ // pixelate() // ------------------------ void pixelate(PImage img) { img.loadPixels(); // ----- scan image for (int x = 0; x < img.width - _resolution; x += _resolution) { for (int y = 0; y < img.height - _resolution; y += _resolution) { // ----- calculate _resolutionaverage float avgR = 0.0; float avgG = 0.0; float avgB = 0.0; for (int col = x; col < x + _resolution; col++) { for (int row = y; row < y + _resolution; row++) { int pixel = col + row * img.width; color c = img.pixels[pixel]; avgR += red(c); // float r = c >> 16 & 0xFF; is faster avgG += green(c); // float g = c >> 8 & 0xFF; is faster avgB += blue(c); // float b = c & 0xFF; is faster } } avgR /= _resolution * _resolution; // avgR = sum/sample-area avgG /= _resolution * _resolution; avgB /= _resolution * _resolution; // ----- pixelate image for (int col = x; col < x + _resolution; col++) { for (int row = y; row < y + _resolution; row++) { int pixel = col + row * img.width; img.pixels[pixel] = color(avgR, avgG, avgB); } } } } img.updatePixels(); } // ------------------------ // dither() // ------------------------ void dither(PImage img) { img.loadPixels(); // ----- perform Floyd-Steinberg dither for (int y = 0; y < img.height - 1; y++) { for (int x = 1; x < img.width - 1; x++) { // ----- extract pixel colors color pix = img.pixels[x + y * img.width]; float oldR = red(pix); float oldG = green(pix); float oldB = blue(pix); // ----- redraw image with fewer colors int factor = 1; // n+1 possible colors int newR = round(factor * oldR / 255) * (255 / factor); int newG = round(factor * oldG / 255) * (255 / factor); int newB = round(factor * oldB / 255) * (255 / factor); img.pixels[x + y * img.width] = color(newR, newG, newB); // https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering // ----- calculate error terms float errR = oldR - newR; float errG = oldG - newG; float errB = oldB - newB; // ----- apply 7/16 color c = img.pixels[(x + 1) + y * img.width]; float r = red(c); float g = green(c); float b = blue(c); r = r + errR * 7/16.0; g = g + errG * 7/16.0; b = b + errB * 7/16.0; img.pixels[(x + 1) + y * img.width] = color(r, g, b); // ----- apply 3/16 c = img.pixels[(x - 1) + (y + 1) * img.width]; r = red(c); g = green(c); b = blue(c); r = r + errR * 3/16.0; g = g + errG * 3/16.0; b = b + errB * 3/16.0; img.pixels[(x - 1) + (y + 1) * img.width] = color(r, g, b); // ----- apply 5/16 c = img.pixels[x + (y + 1) * img.width]; r = red(c); g = green(c); b = blue(c); r = r + errR * 5/16.0; g = g + errG * 5/16.0; b = b + errB * 5/16.0; img.pixels[x + (y + 1) * img.width] = color(r, g, b); // ----- apply 1/16 c = img.pixels[x + (y + 1) * img.width]; r = red(c); g = green(c); b = blue(c); r = r + errR * 1/16.0; g = g + errG * 1/16.0; b = b + errB * 1/16.0; img.pixels[x + (y + 1) * img.width] = color(r, g, b); } } img.updatePixels(); } // ------------------------------------- // traceMono() // ------------------------------------- void traceMono(int X, int Y) { //XY is 3x3 matrix center stroke(0 ,0 , 0, 128); // ----- determine pixel brightness int value = int(brightness(_mono.pixels[X + Y*_mono.width])); // ----- action based on pixel brightness switch (value) { // -------------------------- // adjacent black pixels // -------------------------- case 0 : _mono.pixels[X + Y*_mono.width] = color(0, 0, 250); //change to blue (width = screen width) // ----- generate gcode if ((abs((X-lastX))<2) && (abs((Y-lastY))<2)) { line(lastX*_resolution, lastY*_resolution, X*_resolution, Y*_resolution); // ----- next point within 1 pixel // move to next point (pen down) print("G01 X"); print(X * _gcodeScaleFactor); print(" Y"); print((height - Y) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format monoOutput.println("G01 X" + (X * _gcodeScaleFactor) + " Y" + (height - Y * _gcodeScaleFactor)); lastCommandG00 = false; lastX = X; lastY = Y; } else { // -------------------------- // non-adjacent black pixels // -------------------------- // ----- next point greater than 1 pixel // move to next point (pen up) if (lastCommandG00 == true) { point( X*_resolution, Y*_resolution); //-----plot last point (pen down) print("G01 X"); print(lastX * _gcodeScaleFactor); print(" Y"); print(height - lastY * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format monoOutput.println("G01 X" + (lastX * _gcodeScaleFactor) + " Y" + ((height - lastY) * _gcodeScaleFactor)); } // ----- move to next point (pen up) println(""); print("G00 X"); print(X * _gcodeScaleFactor); print(" Y"); print((height - Y) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format monoOutput.println("G00 X" + (X * _gcodeScaleFactor) + " Y" + ((height - Y) * _gcodeScaleFactor)); lastCommandG00 = true; } lastX = X; lastY = Y; break; // -------------------------- // white pixels // -------------------------- case 255 : _mono.pixels[X + Y*_mono.width] = color(230); //change to light gray break; // -------------------------- // visited pixels // -------------------------- default : break; } // ----- recursive trace while ((recursionFlag == true) && (recursionDepth-- > 0)) { recursionFlag = false; //routine will exit unless a zero is found // ----- scan the matrix for (int j = -1; j < 2; j++) { //vert scan for (int i = -1; i < 2; i++) { //hor scan // ----- location of the pixel being examined int x = X+i; int y = Y+j; x = constrain(x, 1, _mono.width-1); y = constrain(y, 1, _mono.height-1); // ----- recursive trace if zero found value = int(brightness(_mono.pixels[x + y*_mono.width])); if (value == 0) { recursionFlag = true; //zero found traceMono(x, y); } //end recursive trace } //end hor scan } //end vert scan } //end while } // ------------------------------------- // traceCyan() // ------------------------------------- void traceCyan(int X, int Y) { //XY is 3x3 matrix center stroke(0, 255, 255, 128); // ----- determine pixel brightness int value = int(brightness(_cyan.pixels[X + Y * _cyan.width])); // ----- action based on pixel brightness switch (value) { // -------------------------- // adjacent black pixels // -------------------------- case 0 : _cyan.pixels[X + Y*_cyan.width] = color(0, 0, 250); //change to blue (width = screen width) // ----- generate gcode if ((abs((X-lastX)) < 2) && (abs((Y-lastY)) < 2)) { line(lastX * _resolution, lastY * _resolution, X * _resolution, Y * _resolution); // ----- next point within 1 pixel // move to next point (pen down) print("G01 X"); print(X * _gcodeScaleFactor); print(" Y"); print((height - Y) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format cyanOutput.println("G01 X" + (X * _gcodeScaleFactor) + " Y" + ((height - Y) * _gcodeScaleFactor)); lastCommandG00 = false; lastX = X; lastY = Y; } else { // -------------------------- // non-adjacent black pixels // -------------------------- // ----- next point greater than 1 pixel // move to next point (pen up) if (lastCommandG00 == true) { point( X*_resolution, Y*_resolution); //-----plot last point (pen down) print("G01 X"); print(lastX * _gcodeScaleFactor); print(" Y"); print((height - lastY) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format cyanOutput.println("G01 X" + (lastX * _gcodeScaleFactor) + " Y" + ((height - lastY) * _gcodeScaleFactor)); } // ----- move to next point (pen up) println(""); print("G00 X"); print(X * _gcodeScaleFactor); print(" Y"); print((height - Y) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format cyanOutput.println("G00 X" + (X * _gcodeScaleFactor) + " Y" + ((height - Y) * _gcodeScaleFactor)); lastCommandG00 = true; } lastX = X; lastY = Y; break; // -------------------------- // white pixels // -------------------------- case 255 : _cyan.pixels[X + Y * _cyan.width] = color(230); //change to light gray break; // -------------------------- // visited pixels // -------------------------- default : break; } // ----- recursive trace while ((recursionFlag == true) && (recursionDepth-- > 0)) { recursionFlag = false; //routine will exit unless a zero is found // ----- scan the matrix for (int j = -1; j < 2; j++) { //vert scan for (int i = -1; i < 2; i++) { //hor scan // ----- location of the pixel being examined int x = X+i; int y = Y+j; x = constrain(x, 1, _cyan.width - 1); y = constrain(y, 1, _cyan.height - 1); // ----- recursive trace if zero found value = int(brightness(_cyan.pixels[x + y * _cyan.width])); if (value == 0) { recursionFlag = true; //zero found traceCyan(x, y); } //end recursive trace } //end hor scan } //end vert scan } //end while } // ------------------------------------- // traceMagenta() // ------------------------------------- void traceMagenta(int X, int Y) { //XY is 3x3 matrix center stroke(255, 0, 255, 128); // ----- determine pixel brightness int value = int(brightness(_magenta.pixels[X + Y*_magenta.width])); // ----- action based on pixel brightness switch (value) { // -------------------------- // adjacent black pixels // -------------------------- case 0 : _magenta.pixels[X + Y * _magenta.width] = color(0, 0, 250); //change to blue (width = screen width) // ----- generate gcode if ((abs((X-lastX))<2) && (abs((Y-lastY))<2)) { line(lastX * _resolution, lastY * _resolution, X * _resolution, Y * _resolution); // ----- next point within 1 pixel // move to next point (pen down) print("G01 X"); print(X * _gcodeScaleFactor); print(" Y"); print((height - Y) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format magentaOutput.println("G01 X" + (X * _gcodeScaleFactor) + " Y" + ((height - Y) * _gcodeScaleFactor)); lastCommandG00 = false; lastX = X; lastY = Y; } else { // -------------------------- // non-adjacent black pixels // -------------------------- // ----- next point greater than 1 pixel // move to next point (pen up) if (lastCommandG00 == true) { point( X * _resolution, Y * _resolution); //-----plot last point (pen down) print("G01 X"); print(lastX * _gcodeScaleFactor); print(" Y"); print((height - lastY) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format magentaOutput.println("G01 X" + (lastX * _gcodeScaleFactor) + " Y" + ((height - lastY) * _gcodeScaleFactor)); } // ----- move to next point (pen up) println(""); print("G00 X"); print(X * _gcodeScaleFactor); print(" Y"); print((height - Y) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format magentaOutput.println("G00 X" + (X * _gcodeScaleFactor) + " Y" + ((height - Y) * _gcodeScaleFactor)); lastCommandG00 = true; } lastX = X; lastY = Y; break; // -------------------------- // white pixels // -------------------------- case 255 : _magenta.pixels[X + Y * _magenta.width] = color(230); //change to light gray break; // -------------------------- // visited pixels // -------------------------- default : break; } // ----- recursive trace while ((recursionFlag == true) && (recursionDepth-- > 0)) { recursionFlag = false; //routine will exit unless a zero is found // ----- scan the matrix for (int j = -1; j < 2; j++) { //vert scan for (int i = -1; i < 2; i++) { //hor scan // ----- location of the pixel being examined int x = X+i; int y = Y+j; x = constrain(x, 1, _magenta.width-1); y = constrain(y, 1, _magenta.height-1); // ----- recursive trace if zero found value = int(brightness(_magenta.pixels[x + y*_magenta.width])); if (value == 0) { recursionFlag = true; //zero found traceYellow(x, y); } //end recursive trace } //end hor scan } //end vert scan } //end while } // ------------------------------------- // traceYellow() // ------------------------------------- void traceYellow(int X, int Y) { //XY is 3x3 matrix center stroke(255, 255, 0, 128); // ----- determine pixel brightness int value = int(brightness(_yellow.pixels[X + Y*_yellow.width])); // ----- action based on pixel brightness switch (value) { // -------------------------- // adjacent black pixels // -------------------------- case 0 : _yellow.pixels[X + Y * _yellow.width] = color(0, 0, 250); //change to blue (width = screen width) // ----- generate gcode if ((abs((X-lastX))<2) && (abs((Y-lastY))<2)) { line(lastX * _resolution, lastY * _resolution, X * _resolution, Y * _resolution); // ----- next point within 1 pixel // move to next point (pen down) print("G01 X"); print(X * _gcodeScaleFactor); print(" Y"); print((height - Y) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format yellowOutput.println("G01 X" + (X * _gcodeScaleFactor) + " Y" + ((height - Y) * _gcodeScaleFactor)); lastCommandG00 = false; lastX = X; lastY = Y; } else { // -------------------------- // non-adjacent black pixels // -------------------------- // ----- next point greater than 1 pixel // move to next point (pen up) if (lastCommandG00 == true) { point( X * _resolution, Y * _resolution); //-----plot last point (pen down) print("G01 X"); print(lastX * _gcodeScaleFactor); print(" Y"); print((height - lastY) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format yellowOutput.println("G01 X" + (lastX * _gcodeScaleFactor) + " Y" + ((height - lastY) * _gcodeScaleFactor)); } // ----- move to next point (pen up) println(""); print("G00 X"); print(X * _gcodeScaleFactor); print(" Y"); print((height - Y) * _gcodeScaleFactor); print("\n"); // ----- save output to file in inkscape format yellowOutput.println("G00 X" + (X * _gcodeScaleFactor) + " Y" + ((height - Y) * _gcodeScaleFactor)); lastCommandG00 = true; } lastX = X; lastY = Y; break; // -------------------------- // white pixels // -------------------------- case 255 : _yellow.pixels[X + Y * _yellow.width] = color(230); //change to light gray break; // -------------------------- // visited pixels // -------------------------- default : break; } // ----- recursive trace while ((recursionFlag == true) && (recursionDepth-- > 0)) { recursionFlag = false; //routine will exit unless a zero is found // ----- scan the matrix for (int j = -1; j < 2; j++) { //vert scan for (int i = -1; i < 2; i++) { //hor scan // ----- location of the pixel being examined int x = X+i; int y = Y+j; x = constrain(x, 1, _yellow.width-1); y = constrain(y, 1, _yellow.height-1); // ----- recursive trace if zero found value = int(brightness(_yellow.pixels[x + y * _yellow.width])); if (value == 0) { recursionFlag = true; //zero found traceMagenta(x, y); } //end recursive trace } //end hor scan } //end vert scan } //end while }