Posterization filter explained?

Here it is!

I implemented three different palettes:

  • Random palette (with random colors)
  • Standard palette (RGB cube)
  • Adaptive palette (with median cut algorithm)

You can click on the different buttons to switch the palette and press - or + to add/ remove colors from the palette.

/**
 * Quantize colors interface
 * Topic: https://discourse.processing.org/t/posterization-filter-explained
 * Joseph HENRY
 */

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Generic class for a palette
 */
abstract class Palette {
  IntList colors;

  Palette() {
    colors = new IntList();
  }

  void addColor(color c) {
    colors.append(c);
  }

  void addRandomColor() {
    colors.append(color(random(255), random(255), random(255)));
  }

  color getClosestColorTo(color c) {
    int minIndex = 0;
    float minDistance = distanceBetweenColorsSq(c, colors.get(0));

    for (int i = 1; i < colors.size(); i++) {
      float distance = distanceBetweenColorsSq(c, colors.get(i));

      if (distance < minDistance) {
        minIndex = i;
        minDistance = distance;
      }
    }

    return colors.get(minIndex);
  }

  abstract String toString();
  abstract void increase();
  abstract void decrease();

  void display(int x, int y, int w, int h) {
    int xSquares = 1;

    while (xSquares * ((h * xSquares) / (float) w) < colors.size()) {
      xSquares++;
    }

    float s = (float) w / xSquares;
    int ySquares = int(h / s);

    stroke(0);

    pushMatrix();
    translate(x, y);
    for (int i = 0; i < xSquares; i++) {
      for (int j = 0; j < ySquares; j++) {
        int loc = i + j * xSquares;
        if (loc >= colors.size()) break;
        fill(colors.get(loc));
        square(i * s, j * s, s);
      }
    }
    popMatrix();
  }
}

/**
 * Palette with random colors
 */
class RandomPalette extends Palette {
  RandomPalette(int n) {
    for (int i = 0; i < n; i++) {
      addRandomColor();
    }
  }

  void increase() {
    addRandomColor();
  }

  void decrease() {
    colors.pop();
  }

  String toString() {
    return "Random palette (" + colors.size() + " colors)";
  }
}

/**
 * Standard palette with RGB cube
 */
class StandardPalette extends Palette {
  int power;

  StandardPalette(int power) {
    this.power = power;
    computeColors();
  }

  void increase() {
    power++;
    computeColors();
  }

  void decrease() {
    power--;
    if (power <= 0) power = 1;
    computeColors();
  }

  /**
   * Compute colors by dividing a 3D RGB cube
   */
  void computeColors() {
    colors.clear();

    float offset = (float) 255 / power;

    for (int x = 0; x <= power; x++) {
      float r = x * offset;
      for (int y = 0; y <= power; y++) {
        float g = y * offset;
        for (int z = 0; z <= power; z++) {
          addColor(color(r, g, z * offset));
        }
      }
    }
  }

  String toString() {
    return str(power * 3) + " bit palette";
  }
}

/**
 * Used to sort a list of colors
 */
class ColorComparator implements Comparator<Integer> {
  int shift;

  ColorComparator(int shift) {
    this.shift = shift;
  }

  int compare(Integer o1, Integer o2) {
    float a = ((o1 >> shift) & 0xFF);
    float b = ((o2 >> shift) & 0xFF);
    if (a < b) return -1;
    else if (a > b) return 1;
    else return 0;
  }
}

/**
 * Palette where the colors are statistically choosen
 */
class AdaptivePalette extends Palette {
  PImage fromImg;
  int n;

  AdaptivePalette(PImage img, int n) {
    this.n = n;
    this.fromImg = img.copy();
    compute();
  }

  void increase() {
    n *= 2;
    compute();
  }

  void decrease() {
    n /= 2;
    if (n <= 1) n = 1;
    compute();
  }
  
  /**
   * Implementation of the median cut algorithm
   * See: https://en.wikipedia.org/wiki/Median_cut
   */
  void compute() {
    colors.clear();

    fromImg.loadPixels();
    List<List<Integer>> buckets = new ArrayList();

    // Put all the pixels into a bucket
    buckets.add(new ArrayList());
    for (int i = 0; i < fromImg.pixels.length; i++) buckets.get(0).add(fromImg.pixels[i]);

    // Continue dividing buckets until we reach the color number target
    while (buckets.size() != n) {
      List<List<Integer>> newBuckets = new ArrayList();

      for (List<Integer> bucket : buckets) {
        // Compute the greatest range between RGB
        float minR = 256;
        float maxR = -1;
        float minG = 256;
        float maxG = -1;
        float minB = 256;
        float maxB = -1;

        for (int i = 0; i < bucket.size(); i++) {
          color c = bucket.get(i);
          float r = (c >> 16) & 0xFF;
          float g = (c >> 8) & 0xFF;
          float b = (c & 0xFF);

          if (r < minR) minR = r;
          if (r > maxR) maxR = r;

          if (g < minG) minG = g;
          if (g > maxG) maxG = g;

          if (b < minB) minB = b;
          if (b > maxB) maxB = b;
        }

        float rangeR = maxR - minR;
        float rangeG = maxG - minG;
        float rangeB = maxB - minB;

        // Store the shift so we can compare the colors later
        int shift = 0;
        if (rangeR > rangeG && rangeR > rangeB) {
          shift = 16;
        } else if (rangeG > rangeR && rangeG > rangeB) {
          shift = 8;
        }

        // Sort the bucket
        Collections.sort(bucket, new ColorComparator(shift));

        // Cut the bucket in half
        int middle = bucket.size() / 2;
        newBuckets.add(bucket.subList(0, middle));
        newBuckets.add(bucket.subList(middle, bucket.size()));
      }

      // Replace by the new buckets
      buckets = newBuckets;
    }

    // Compute the average colors for each bucket
    for (List<Integer> bucket : buckets) {
      float rSum = 0;
      float gSum = 0;
      float bSum = 0;

      for (Integer c : bucket) {
        rSum += (c >> 16) & 0xFF;
        gSum += (c >> 8) & 0xFF;
        bSum += c & 0xFF;
      }

      colors.append(color(
        (float) rSum / bucket.size(), 
        (float) gSum / bucket.size(), 
        (float) bSum / bucket.size()));
    }
  }

  String toString() {
    return "Adaptive palette (" + n + " colors)";
  }
}

/**
 * Display an image with squares (to remove filtering)
 */
void displayImage(PImage img, String legend, int xPos, int yPos, int scale) {
  noStroke();

  pushMatrix();
  translate(xPos, yPos);

  // Display squares
  for (int x = 0; x < img.width; x++) {
    for (int y = 0; y < img.height; y++) {
      fill(img.pixels[x + y * img.width]);
      square(x * scale, y  * scale, scale);
    }
  }

  // Display legend
  textAlign(CENTER, CENTER);
  fill(255);
  textSize(30);
  text(legend, (img.width * scale) / 2.0, (img.height * scale) - 50);

  popMatrix();
}

/**
 * Computes the distance squared between two colors
 */
float distanceBetweenColorsSq(color c1, color c2) {
  float r2 = (c2 >> 16) & 0xFF;
  float g2 = (c2 >> 8) & 0xFF;
  float b2 = (c2 & 0xFF);

  float r1 = (c1 >> 16) & 0xFF;
  float g1 = (c1 >> 8) & 0xFF;
  float b1 = (c1 & 0xFF);

  return (r2 - r1) * (r2 - r1) + 
    (g2 - g1) * (g2 - g1) + 
    (b2 - b1) * (b2 - b1);
}

/**
 * Color quantize an image with the given palette
 */
PImage quantizeWithPalette(PImage img, Palette palette) {
  PImage copy = img.copy();

  img.loadPixels();
  copy.loadPixels();

  for (int x = 0; x < copy.width; x++) {
    for (int y = 0; y < copy.height; y++) {
      int loc = x + y * copy.width;
      copy.pixels[loc] = palette.getClosestColorTo(img.pixels[loc]);
    }
  }

  return copy;
}

/**
 * Clickable button class
 */
class Button {
  String label;
  int x, y;
  float w, h;
  color bgColor;
  int fontSize = 20;

  Button(String label, int x, int y, color bgColor) {
    this.label = label;
    this.x = x;
    this.y = y;
    this.bgColor = bgColor;
    this.w = textWidth(label) + 10;
    this.h = fontSize * 1.5;
  }

  boolean isMouseHover() {
    return mouseX > x - w / 2 &&
           mouseX < x + w / 2 &&
           mouseY > y - h / 2 &&
           mouseY < y + h / 2;
  }

  void display() {
    display(false);
  }

  void display(boolean active) {
    pushStyle();

    textSize(fontSize);

    rectMode(CENTER);

    if (isMouseHover()) {
      stroke(255);
    } else {
      noStroke();
    }

    if (active) fill(#40805b);
    else fill(bgColor);

    rect(x, y, textWidth(label) + 10, fontSize * 1.7, 5);

    textAlign(CENTER, CENTER);
    fill(255);
    text(label, x, y - 5);

    popStyle();
  }
}

PImage img, quantized;

int currentPaletteIndex = 0;
List<Palette> palettes;

List<Button> buttons;
Button decrease, increase;

Palette currentPalette() {
  return palettes.get(currentPaletteIndex);
}

void quantizeImage() {
  quantized = quantizeWithPalette(img, currentPalette());
}

void setup() {
  size(1200, 700);

  img = loadImage("https://upload.wikimedia.org/wikipedia/commons/d/d7/RGB_24bits_palette_sample_image.jpg");

  palettes = new ArrayList<Palette>() {
    {
      add(new StandardPalette(1));
      add(new RandomPalette(8));
      add(new AdaptivePalette(img, 8));
    }
  };

  quantizeImage();

  // Buttons
  final int buttonYPos = height - 50;
  buttons = new ArrayList<Button>() {
    {
      add(new Button("Standard palette", 100, buttonYPos, color(100)));
      add(new Button("Random palette", 300, buttonYPos, color(100)));
      add(new Button("Adaptive palette", 500, buttonYPos, color(100)));
    }
  };

  decrease = new Button("-", 650, buttonYPos, color(#574fc3));
  increase = new Button("+", 700, buttonYPos, color(#fe3548));
}

void draw() {
  background(50);

  displayImage(img, "24 bit palette", 0, 0, 3);
  displayImage(quantized, currentPalette().toString(), 450, 0, 3);

  currentPalette().display(900, 0, 300, 600);

  for (int i = 0; i < buttons.size(); i++) {
    buttons.get(i).display(i == currentPaletteIndex);
  }

  decrease.display();
  increase.display();
}

void mousePressed() {
  for (int i = 0; i < buttons.size(); i++) {
    Button button = buttons.get(i);

    if (button.isMouseHover()) {
      currentPaletteIndex = i;
      quantizeImage();
      break;
    }
  }
  
  if (decrease.isMouseHover()) {
    currentPalette().decrease();
    quantizeImage();
  }
  
  if (increase.isMouseHover()) {
    currentPalette().increase();
    quantizeImage();
  }
}

void keyPressed() {
  if (keyCode == RIGHT) {
    currentPalette().increase();
    quantizeImage();
  } else if (keyCode == LEFT) {
    currentPalette().decrease();
    quantizeImage();
  }
}
2 Likes