Code to compare pixels by color

I need a script that does the following:
The script should look at an image pixels by pixels.
For each pixel, the script has to look for an other pixel with the same rgb value in a different image. (Possibly with a small tolerance). If in the 2nd image is such a pixel, this pixel must be deleted. Only one pixels may be erased per comparison.
My questions to you are.

  1. Is my description clear?
  2. Is such a thing possible with processing, or should I look for another solution.
  3. Is it possible for someone with little or no scripting experience to get this done?

Thanks for your help !!!

1 Like

Three Yes for you

look at red() green () blue () command

and check the difference between the red values of 2 image pixels , the green and blue

Check out examples for loadImage, pixels [] etc.

Thanks for your help. But I doubt if I can write the script myself. At least not in the time I have available at this moment?
How much would it cost to let someone write it for me?

It’s fun to do this, so give it a try. Is this homework? It’s easy when you copy the examples in the reference for said commands

1 Like

Hi

https://funprogramming.org/90-Change-pixel-hue-saturation-and-brightness.html

https://funprogramming.org/89-Create-your-own-photo-filters.html

https://funprogramming.org/88-Change-pixels-using-the-pixels-array.html

https://funprogramming.org/81-How-to-read-the-color-of-a-pixel.html

1 Like

Its not for homework. (I’m way to old for that).
Thanks for the help. It is fun to do, but only 24hours in a day, so somethings it is better to delegate.

1 Like

Do you mean any pixel in image #2 or rather the pixel in the same location?

any pixel in the #2 image

so from image #1 every pixel gets killed that color occurs anywhere in image #2

Yes.
the full description:
The script starts with the first pixel of image #1.
It “reads” the color of that first pixel.
Than it “looks” at image #2:
The script checks every pixel one by one until it finds a pixel with the same color or gets at the end of the image.
If a pixel is found with the same color. That pixel is deleted (made transparent, or white) in image #2. The scripts stops looking for that color, so for every pixels in image #1 only one (or non) pixel is deleted.
The scripts looks at the next pixel in image #1. And it looks at image #2 again to find the same color and delete. Until it has checked every pixel.
Probably the color from image #2 hasn’t got the be exactly the same color as the pixel from image #1. But a small tolerance has be build in.
The idea is that when I (for example) compare an image with little red to an image with lots of red. The most red pixels (and other different colors) “survive”. That image with the surviving pixels it later processed with pixel sorting software.

you mean #1

That’s new. We have to store the removed colors to avoid a 2nd removal. Or reset in image #2.

I can’t do it, no time.

I’m not sure about that. For every pixel in image #1 (source) only (or non) pixel can be deleted. The script stops looking for the color of that pixel. But If there are 5 pixels in image #1 with the same color. The scripts has to look for that same color 5 times.

No problem ad thanks for the help.
How much time would a good coder need to write a script like this?

1 Like

Hello @Marn,

Is this the correct interpretation of what you are trying to do?

A closeup of Image 1:

image

This was my morning coding challenge.

I created a test case of images.
Image 1 is on left.
Image 2 is in the middle.
The processed image is on right.
I used black to fill the pixels.

References:
Images and Pixels / Processing.org
Color / Processing.org

:)

Hey,

I also tried a little something.
Performance are ok for 800x500 pixels images.
It starts to really suffer will 1920*1080 pixels images.

Here are a couple examples of the result (Source on the left, Target in the middle, Result on the right):

Code
import java.util.*;

Octree octree;
ArrayList<Pix> pixToRemove;
PImage sourceImg;
PImage targetImg;
PImage resultImg;

final int dist = 10;



void setup() {
  size(1500, 900);
  background(20);

  octree = new Octree(0, 0, 0, 256);
  pixToRemove = new ArrayList<Pix>();
  sourceImg = loadImage("Source.jpg");
  targetImg = loadImage("Target.jpg");
  resultImg = loadImage("Target.jpg");

  loadTargetPixels();
  getPixToRemove();
  cleanResultImg();
  displayImg();
}



void loadTargetPixels() {
  targetImg.loadPixels();

  int[] indexes = new int[targetImg.width * targetImg.height];
  for (int i = 0; i < targetImg.width * targetImg.height; i++) {
    indexes[i] = i;
  }
  shuffleArray(indexes);

  for (int i = 0; i < indexes.length; i++) {
    int idx = indexes[i];
    int y = idx / targetImg.width;
    int x = idx - y * targetImg.width;
    int r = (targetImg.pixels[idx] >> 16) & 0xFF;
    int g = (targetImg.pixels[idx] >> 8) & 0xFF;
    int b = targetImg.pixels[idx] & 0xFF;  
    Pix p = new Pix(x, y, r, g, b);
    octree.insert(p);
  }
  println("Tree built");
}



static void shuffleArray(int[] ar)
{
  Random rnd = new Random();
  for (int i = ar.length - 1; i > 0; i--)
  {
    int index = rnd.nextInt(i + 1);
    // Simple swap
    int a = ar[index];
    ar[index] = ar[i];
    ar[i] = a;
  }
}



void getPixToRemove() {
  sourceImg.loadPixels();
  for (int j = 0; j < sourceImg.height; j++) {
    for (int i = 0; i < sourceImg.width; i++) {
      int idx = j * sourceImg.width + i;
      int r = (sourceImg.pixels[idx] >> 16) & 0xFF;
      int g = (sourceImg.pixels[idx] >> 8) & 0xFF;
      int b = sourceImg.pixels[idx] & 0xFF;

      Pix toRemove = octree.findClosePt(r, g, b, dist);
      
      if (idx % 5000 == 0) {
        println( nf(idx, 6) + " / " + sourceImg.height * sourceImg.width + " (" + nf(100 * idx / (sourceImg.height * sourceImg.width), 3) + "%)");
      }
      
      if (toRemove == null) continue;
      pixToRemove.add(toRemove);
    }
  }
  println("Pixels to delete found");
}



void cleanResultImg() {
  resultImg.loadPixels();
  for (int i = 0; i < pixToRemove.size(); i++) {
    Pix p = pixToRemove.get(i);
    int idx = p.y * resultImg.width + p.x;
    resultImg.pixels[idx] = color(255);
  }
  resultImg.updatePixels();
  println("Pixels removed");
}



void displayImg() {
  float sourceRatio = sourceImg.width / (float)sourceImg.height;
  float targetRatio = targetImg.width / (float)targetImg.height;
  float maxWidth = width / 3.0;
  float maxHeight = height;

  //Draw source image
  if (sourceRatio >= 1) { //Landscape image
    if (sourceImg.width <= maxWidth) {
      image(sourceImg, 0, 0);
    } else {
      image(sourceImg, 0, 0, maxWidth, maxWidth / sourceRatio);
    }
  } else { //Portrait image
    if (sourceImg.height <= maxHeight) {
      image(sourceImg, 0, 0);
    } else {
      image(sourceImg, 0, 0, maxHeight * sourceRatio, maxHeight);
    }
  }

  //Draw source image
  if (targetRatio >= 1) { //Landscape image
    if (targetImg.width <= maxWidth) {
      image(targetImg, maxWidth, 0);
      image(resultImg, 2 * maxWidth, 0);
    } else {
      image(targetImg, maxWidth, 0, maxWidth, maxWidth / targetRatio);
      image(resultImg, 2 * maxWidth, 0, maxWidth, maxWidth / targetRatio);
    }
  } else { //Portrait image
    if (targetImg.height <= maxHeight) {
      image(targetImg, maxWidth, 0);
      image(resultImg, 2 * maxWidth, 0);
    } else {
      image(targetImg, maxWidth, 0, maxHeight * targetRatio, maxHeight);
      image(resultImg, 2 * maxWidth, 0, maxHeight * targetRatio, maxHeight);
    }
  }
}



// *****************
class XYZ {
  float x, y, z;
  
  public XYZ(float l_x, float l_y, float l_z) {
    x = l_x;
    y = l_y;
    z = l_z;
  }
  
  public XYZ copy() {
    return new XYZ(x, y, z);
  }
  
  public XYZ add(float l_x, float l_y, float l_z) {
    return new XYZ(x + l_x, y + l_y, z + l_z);
  }
}



// *****************
class AABB {
  XYZ corner; //Top left corner
  float size;
  
  public AABB (XYZ l_corner, float l_size) {
    corner = l_corner.copy();
    size = l_size;
  }
  
  public AABB (float l_x, float l_y, float l_z , float l_size) {
    corner = new XYZ(l_x, l_y, l_z);
    size = l_size;
  }
  
  public boolean containsPt(float l_x, float l_y, float l_z) {
    if (l_x < corner.x) return false;
    if (l_x >= corner.x + size) return false;
    if (l_y < corner.y) return false;
    if (l_y >= corner.y + size) return false;
    if (l_z < corner.z) return false;
    if (l_z >= corner.z + size) return false;
    return true;
  }
  
  public boolean intersectAABB(AABB l_other) {
    if (this.corner.x + size <= l_other.corner.x) return false;
    if (this.corner.x >= l_other.corner.x + l_other.size) return false;
    if (this.corner.y + size <= l_other.corner.y) return false;
    if (this.corner.y >= l_other.corner.y + l_other.size) return false;
    if (this.corner.z + size <= l_other.corner.z) return false;
    if (this.corner.z >= l_other.corner.z + l_other.size) return false;
    return true;
  }
  
  public AABB copy() {
    return new AABB(corner, size);
  }
}



//********************
class Pix {
  int x, y;
  int r, g, b;

  public Pix(int l_x, int l_y, int l_r, int l_g, int l_b) {
    x = l_x;
    y = l_y;
    r = l_r;
    g = l_g;
    b = l_b;
  }
}



// *****************
class Octree {
  final private int m_maxCapacity = 8;
  final private int m_maxSize = 5;

  private AABB m_boundary;
  private ArrayList<Pix> m_pixs;
  private Octree[] m_children;



  public Octree(XYZ l_corner, float l_size) {
    m_boundary = new AABB(l_corner, l_size);
    m_pixs = new ArrayList<Pix>();
    m_children = new Octree[8];
  }



  public Octree(float l_x, float l_y, float l_z, float l_size) {
    m_boundary = new AABB(l_x, l_y, l_z, l_size);
    m_pixs = new ArrayList<Pix>();
    m_children = new Octree[8];
  }



  public boolean insert(Pix l_p) {
    //Add only if point is within the boundary
    if (!m_boundary.containsPt(l_p.r, l_p.g, l_p.b)) 
      return false;

    //Add the point if there is space and no children or if the size is the minimum possible
    if ((m_pixs.size() < m_maxCapacity &&  m_children[0] == null) || m_boundary.size < m_maxSize) {
      m_pixs.add(l_p);
      return true;
    }

    //Otherwise subdivide and insert
    if (m_children[0] == null)
      subdivide();

    for (int k = 0; k < 8; k++) {
      if (m_children[k].insert(l_p))
        return true;
    }

    // Should never happen
    return false;
  }



  private void subdivide() {
    float newSize = m_boundary.size / 2.0;
    XYZ corner = m_boundary.corner;

    // Creating the new leaves
    m_children[0] = new Octree(corner, newSize);
    m_children[1] = new Octree(corner.add(newSize, 0, 0), newSize);
    m_children[2] = new Octree(corner.add(0, newSize, 0), newSize);
    m_children[3] = new Octree(corner.add(newSize, newSize, 0), newSize);
    m_children[4] = new Octree(corner.add(0, 0, newSize), newSize);
    m_children[5] = new Octree(corner.add(newSize, 0, newSize), newSize);
    m_children[6] = new Octree(corner.add(0, newSize, newSize), newSize);
    m_children[7] = new Octree(corner.add(newSize, newSize, newSize), newSize);

    // Move parent points to leaves
    for (int i = m_pixs.size() - 1; i >= 0; i--) {
      Pix p = m_pixs.get(i);
      for (int k = 0; k < 8; k++) {
        if (m_children[k].insert(p)) {
          m_pixs.remove(i);
          break;
        }
      }
    }
  }
  
  
  
  public Pix findClosePt(float l_x, float l_y, float l_z, float l_radius) {
    //Create the bounding box of the sphere
    AABB sphereBoundingBox = new AABB(l_x - l_radius, l_y - l_radius, l_z - l_radius, 2 * l_radius);

    return findClosePt(l_x, l_y, l_z, l_radius, sphereBoundingBox);
  }
  
  
  
  public Pix findClosePt(float l_x, float l_y, float l_z, float l_radius, AABB l_sphereBoundingBox) {
    //If the sphere does not intersect with the current node, no need to continue
    if (!m_boundary.intersectAABB(l_sphereBoundingBox))
      return null;

    //Check points in the node
    float maxDistSq = l_radius * l_radius;
    //for (int i = 0; i < m_pixs.size(); i++) {
    for (int i = m_pixs.size() - 1; i >= 0; i--) {
      Pix p = m_pixs.get(i);
      
      float distSq = (p.r - l_x) * (p.r - l_x) + (p.g - l_y) * (p.g - l_y) + (p.b - l_z) * (p.b - l_z);
      if (distSq <= maxDistSq) {
        m_pixs.remove(i);
        return p;
      }
    }

    //If not children, stop recursion
    if (m_children[0] == null) return null;
    
    //Otherwise, look into children
    int[] indexes = {0, 1, 2, 3, 4, 5, 6, 7};
    shuffleArray(indexes);
    for (int k = 0; k < 8; k++) {
      Pix result = m_children[indexes[k]].findClosePt(l_x, l_y, l_z, l_radius, l_sphereBoundingBox);
      if (result != null) {
        return result;
      }
    }
    
    return null;
  }
}
1 Like

@glv and @jb4x
That looks great. Thanks for the effort!
@glv, do you want to share the code?
@jb4x , I’m going to take some a good look at the code an do some tests in the next days

1 Like

I had actually another version prior to this one.

In both version I’m using an octree for spatial partitioning of the pixels of the target image.

In the first version, I was querying all points within a sphere around the source RGB point and then was sorting all those points by distance to the center of the sphere.
This way, I ensured that I would first delete pixels that perfectly match the ones in the source image.
The performance were really bad though because of the ArrayList addAll method.

My way around it was to simply pick the first point found within the sphere. This way I got rid of the addAll method and gain some performances. The remove method of the arrayList is still a pain point but its wayy better than the first version.
The drawback of couse is that it is possible that some pixels do not get deleted in the target image even though there is a perfect match on the source image while another pixel without a perfect match but close to the color gets deleted.

In the second version, I also shuffled the target pixels before adding them to the octree to avoid the effect you can see on @glv’s answer where several pixels in a row gets deleted while the other stays.

1 Like

Hello,

@Marn,

You did not answer my question.

I implemented my interpretation of this:

The pixels were were read into a pixel array and replaced in a pixel array; left to right and top to bottom of the image.
I made it black instead of white or transparent.

:)

@glv, sorry but yes it looks it is the correct interpretation.

@jb4x
it is not a problem that the pixels are deleted in a row. The pixels of the image get sorted later in the proces by imagesorting software.

It tried the script with some images and I looks good.
When I use the same image as Source and Target it expect to get a total white image, but the script doesn’t kill all the pixels. Do you understand why?

@jb4x and @glv
I real appreciated what you guys are doing. I started myself with writing and all had after several hours is a script that can read the pixels of the source image.

As I explained in my previous post, for performance reasons I now delete the first pixel I find that matches (even though it is not exactly the same color) so the algorithm might miss some perfect match.

Let’s take an example in 2D space:
image

We have 3 points (Red, Purple and Yellow).
Both the source image and the target image have the same point.
The circles correspond to the area were 2 points are considered identical. (You can change that in my code by changing the dist variable)

Now, let’s imagine we read the source image in the following order:

  1. Red point: there are 2 possible match, the Red (of course) but also the Purple. And for some reasons, the purple is found first so it got deleted on the target images (Remain Red and Yellow now).
  2. Purple point: Red, Purple and Yellow are a match. As Purple is already deleted, we could delete either the Red or the Yellow. The Yellow is found first so it gets deleted so only the Red is remaining.
  3. Yellow point: Yellow and Purple are a match but they were both already removed so no additional points were removed.

You can see with that example that because of the way the algorithm works, even if we start we 2 identical set of points, it is possible to not delete all of them. That was not the case with the first version of the algorithm but again, performances were really poor.

Also note that if you set dist to 0 is should get rid of all the pixels.

It’s been a while but I kept thinking of your little challenge and came up with a better version I think.

This time, the algorithm will always match the best possible pixels together (the closest ones) so if you input twice the same pictures, every pixel gets deleted.

Here’s how it works:

  1. The first step is to look up all the target pixels and remove all the doubles (in terms of colors)

  2. Once that’s done a KDTree is built (with 3 dimensions being the R, G and B channels of the colors) using that set of pixels. Each node correspond to a unique color in the target image. For those interested, I used the technique explained in this paper to get a balanced tree.

  3. All the target pixels are “added” to the node corresponding to their color. The goal is to have, for each node, the list of all available pixel of the same color that we can remove.

  4. The next step is to “add” each pixel from the source image to the tree by finding the closest suitable node. To find that best matching node, a variant of a nearest neighbor search is used with the following rules:

    1. On a node, if there are still some target pixels available to be removed: the algorithm continue as usual
    2. If on the other hand, there are no more target pixels that can be removed, 2 options:
      1. If the distance to the node is better than the worse distance previously matched: the algorithm continue as usual
      2. If the distance is not better than the worse one, the algorithm ignore that node

    When the best node is found, there are still 2 options:

    1. There is still at least one target pixel that can be removed and in that case we pair that pixel to the source color
    2. There is no target pixels left to be removed. In that case, the worst color is replaced by the new one and the algorithm look for a new math for the removed color.
  5. When all the pixels to be removed have been identified, the tree is iterated over one last time to remove the matched pixels from the target image.

The performance are greatly improved compare to my last post. I managed to deal with a 4K by 6K image quite fast. The 2 main factors impacted the performance are the size of the source image as well as the threshold used to consider if there is a match or not.

Here an example outcome with a threshold = 100 (the threshold represent the distance squared).

Source image:

Target image:

Result image:

I fully commented the code and described as much as possible the mechanics so it shouldn’t be to hard to understand how the code is structured and how it works.

Summary
import java.util.*; 

PixelProcessor pp;

void setup() {
    PImage targetImg, srcImg, processedImg;

    targetImg = loadImage("Target_02.jpg");
    srcImg = loadImage("Source_02.jpg");
    
    pp = new PixelProcessor(targetImg);
    processedImg = pp.removePixelWith(srcImg, 100);
    
    processedImg.save("data/Result_02.jpg");
    println("Processed image saved");
}


/**
* This class is a lightweight class used to keep track of the RGB channels of a color.
* It aims at facilitating the comparaison of the colors and the computation of distances to other colors or plane.
*/
class PixelColor {
  public byte r, g, b;
  
  /*
  * Class constructor from an int encoding a color where the bits are ordered as followed: ********RRRRRRRRGGGGGGGGBBBBBB.
  * R's contain the red value, G's the green and B's the blue one.
  */
  public PixelColor(int l_rgb) {
    r = (byte)(l_rgb >> 16);
    g = (byte)(l_rgb >> 8);
    b = (byte)l_rgb;
  }
  
  /*
  * Class constructor from each individual channel.
  */
  public PixelColor(int l_r,int l_g,int l_b) {
    r = (byte)l_r;
    g = (byte)l_g;
    b = (byte)l_b;
  }
  
  /*
  * Print the color in the console with format "(RRR, GGG, BBB)"
  */
  public void prettyPrint() {
    print("(" + nf(0xFF & r, 3) + ", " + nf(0xFF & g, 3) + ", " + nf(0xFF & b, 3) + ")");
  }
  
  /*
  * Combine each color channel in an int with the bits ordered as followed: 00000000RRRRRRRRGGGGGGGGBBBBBBBB
  * R's contain the red value, G's the green and B's the blue one.
  */
  public int getInt() {
    return ((0xFF & r) << 16) | ((0xFF & g) << 8) | (0xFF & b);
  }
  
  /*
  * Check if each channel of the colors are equal.
  *
  * @param  l_o   The other color to compare to
  * @return       true if each channels are equals, false otherwise
  */
  public boolean isEqualTo(PixelColor l_o) {
    return RGBCompareTo(l_o) == 0;
  }
  
  /*
  * Compare two colors for order.
  * The comparaison is done on super keys: RGB, GBR or BRG. Meaning each color is converted in an int with the bits ordered as specified before comparing the 2 values.
  *
  * @param  l_o          The other color to compare to
  * @param  l_sortOrder  The order in which to arrange the bits
  *                         1: bits ordered as RGB: 00000000RRRRRRRRGGGGGGGGBBBBBBBB
  *                         2: bits ordered as GBR: 00000000GGGGGGGGBBBBBBBBRRRRRRRR
  *                         3: bits ordered as BRG: 00000000BBBBBBBBRRRRRRRRGGGGGGGG
  *                         Any other values than 1 or 2 will end up using the GBR order
  * @return              A positive value, zero or a negative value depending if the current color is bigger, equal or smaller than the color argument
  */
  public int compareTo(PixelColor l_o, int l_sortOrder) {
    if (l_sortOrder == 0) return RGBCompareTo(l_o);
    if (l_sortOrder == 1) return GBRCompareTo(l_o);
    return BRGCompareTo(l_o);
  }
  
  /*
  * Compare two colors for order.
  * The comparaison is done on the super key: RGB. Meaning each color is converted in an int with the bits ordered as follow 00000000RRRRRRRRGGGGGGGGBBBBBBBB
  *
  * @param  l_o   The color with which to compare
  * @return       A positive value, zero or a negative value depending if the current color is bigger, equal or smaller than the color argument
  */
  public int RGBCompareTo(PixelColor l_o) {
    return (((0xFF & r) << 16) | ((0xFF & g) << 8) | (0xFF & b)) - (((0xFF & l_o.r) << 16) | ((0xFF & l_o.g) << 8) | (0xFF & l_o.b));
  }
  
  /*
  * Compare two colors for order.
  * The comparaison is done on the super key: GBR. Meaning each color is converted in an int with the bits ordered as follow 00000000GGGGGGGGBBBBBBBBRRRRRRRR
  *
  * @param  l_o   The color with which to compare
  * @return       A positive value, zero or a negative value depending if the current color is bigger, equal or smaller than the color argument
  */
  
  public int GBRCompareTo(PixelColor l_o) {
    return (((0xFF & g) << 16) | ((0xFF & b) << 8) | (0xFF & r)) - (((0xFF & l_o.g) << 16) | ((0xFF & l_o.b) << 8) | (0xFF & l_o.r));
  }
  
  /*
  * Compare two colors for order.
  * The comparaison is done on the super key: BRG. Meaning each color is converted in an int with the bits ordered as follow 00000000BBBBBBBBRRRRRRRRGGGGGGGG
  *
  * @param  l_o   The color with which to compare
  * @return       A positive value, zero or a negative value depending if the current color is bigger, equal or smaller than the color argument
  */
  public int BRGCompareTo(PixelColor l_o) {
    return (((0xFF & b) << 16) | ((0xFF & r) << 8) | (0xFF & g)) - (((0xFF & l_o.b) << 16) | ((0xFF & l_o.r) << 8) | (0xFF & l_o.g));
  }
  
  /*
  * A color can be thought as a point in 3D space where R, G and B are the unit vectors of the space.
  * Compute the distance squared between the color and a plane normal to one of the R, G or B direction going through a given color.
  *
  * @param  l_o        Color belonging to the plane
  * @param  l_normal   The direction of the normal to the plane
  *                       1: the normal is the R direction
  *                       2: the normal is the G direction
  *                       3: the normal is the B direction
  *                       Any other values will return 0
  * @return            The distance squared
  */
  public int distFromPlane(PixelColor l_o, int l_normal) {
    int delta = 0;
    if (l_normal == 0) delta = (0xFF & r) - (0xFF & l_o.r);
    if (l_normal == 1) delta = (0xFF & g) - (0xFF & l_o.g);
    if (l_normal == 2) delta = (0xFF & b) - (0xFF & l_o.b);
    return delta * delta;
  }
  
  /*
  * A color can be thought as a point in 3D space where R, G and B are the unit vectors of the space.
  * Compute the distance squared between 2 colors.
  *
  * @param  l_o   The color with which to compute the distance
  * @return       The distance squared
  */
  public int distFrom(PixelColor l_o) {
    int dr = (0xFF & r) - (0xFF & l_o.r);
    int dg = (0xFF & g) - (0xFF & l_o.g);
    int db = (0xFF & b) - (0xFF & l_o.b);
    return dr * dr + dg * dg + db * db;
  }
}


/**
* A simple classe to hold the (x, y) coordinates of a pixel
*/
class PixelCoord {
  public int x, y;
  
  /*
  * Class constructor.
  */
  public PixelCoord(int l_x, int l_y) {
    x = l_x;
    y = l_y;
  }
}


/*
* Class designed to work in pair with the Node class.
* It allows to keep track of which pixels on the target image shares the same color as the node that created the object and if it should be deleted during the post processing or not.
* To know weither or not a target pixel should be deleted, the pixels from the source image are analysed and paired against target pixel with the closest color.
* If a target pixel is paired, it should be deleted, otherwise it is not.
* Of course it is not possible to pair more source pixel that there are available target pixels with a given color.
* If it happens, the distance from that source pixel is compared to the one already matched and if the distance is smaller (a better match) then this source pixel is added and the other one checked against the next closet target pixel.
* 
* This class offers method to help identifying when all the target pixels are paired and help pairing new source pixels.
*/
class PixelPairings {
  public PixelCoord[] m_targetCoords;  // Coordinates of all the pixels of the target image sharing the same color of the node using that object
  public PixelColor[] m_pairedColors;  // Colors of the source image that got paired with the target image color with ascending order of distances from the target color of this object. 
  public int[] m_pairedColorsDist;     // Distance squared from the colors that got paired to the color of the node using that object with ascending order. Always match the m_pairedColors array to give the corresponding distance.
  public int m_nbOfPairedColors;       // Number of colors that got paired so fare
  public int m_nbOfTargetCoords;       // Number of pixels from the target image that got added so far
  public int m_size;                   // Size of the 3 arrays
  
  /*
  * Class constructor.
  * It takse as input the number of target pixels sharing the node color to give the proper size to the arrays.
  */
  public PixelPairings(int l_size) {
    m_size = l_size;
    m_nbOfPairedColors = 0;
    m_nbOfTargetCoords = 0;
    m_targetCoords = new PixelCoord[l_size];
    m_pairedColorsDist = new int[l_size];
    m_pairedColors = new PixelColor[l_size];
    initDistances();
  }
  
  /*
  * The m_pairedColorsDist is used to store the distance squared from the source colors that got paired to this object target color.
  * The distances are stored in ascending order. 
  * To know if a new source color should be added, it is simply a matter of checking the last distance of that array with the new source color distance. 
  * If it is smaller then the new color need to be added.
  * For this reason all the distances are initialized with a value higher than the maximum possible value wich is: 3 * 255 * 255 * 255 = 49 744 125
  * It is also usefull to detect target point that got paired as the distance will necessarily be lower than the max value. 
  */
  private void initDistances() {
    for (int i = 0; i < m_size; i++) {
      m_pairedColorsDist[i] = 50000000;
    }
  }
  
  /*
  * Add the coordinate of a target pixel sharing the same color as the node that created this object.
  * There is no check on out of bound insertion since the size of the array has been defined knowing how many pixels in the target image share the node color.
  *
  * @param  l_coord   The target pixel coordinate to add to the object
  */  
  public void addTargetPixelCoord(PixelCoord l_coord) {
    m_targetCoords[m_nbOfTargetCoords] = l_coord;
    m_nbOfTargetCoords++;
  }
  
  /*
  * Check if a new source color distance to the node color is smaller than the biggest distance from the source colors currently paired.
  *
  * @param  l_d   The new source color distance
  * @return       true if the new source color distance is smaller than the biggest distance from the source colors currently paired, false otherwise
  */ 
  public boolean isBetterMatch(int l_d) {
    if (m_nbOfPairedColors < m_size) return true;
    if (l_d < m_pairedColorsDist[m_size - 1]) return true;
    return false;
  }
  
  /*
  * Add a new source color to the paired source colors array.
  * It keeps the paired source colors and distances arrays paired and ensure that they are sorted by ascending order leaving the furthest source colors in the back of the array.
  * It also return the paired source color that got deleted if any in order to be able to pair that color with another target pixel.
  *
  * @param  l_t   The source pixel color to pair
  * @param  l_d   The distance from that source color to the node color that created that object
  * @return       The furthest paired color that got removed from the array if any, null otherwise
  */ 
  public PixelColor addSourcePoint(PixelColor l_color, int l_d) {   
    PixelColor toReturn = m_pairedColors[m_size - 1];
    
    // Array of any size but no element in it
    if (m_nbOfPairedColors == 0) {
      m_pairedColors[0] = l_color;
      m_pairedColorsDist[0] = l_d;
      m_nbOfPairedColors++;
      return toReturn;
    }
    
    // Array of size 1 with 1 element in it
    if (m_size == 1) {
      if (l_d < m_pairedColorsDist[0]) {
        m_pairedColors[0] = l_color;
        m_pairedColorsDist[0] = l_d;
        return toReturn;
      } else {
        return null;
      }
    }
    
    // Array of size >= 2 with only 1 element in it
    if (m_nbOfPairedColors == 1) {
      if (l_d < m_pairedColorsDist[0]) {
        m_pairedColors[1] = m_pairedColors[0];
        m_pairedColorsDist[1] = m_pairedColorsDist[0];
        
        m_pairedColors[0] = l_color;
        m_pairedColorsDist[0] = l_d;
        
        m_nbOfPairedColors++;
        return toReturn;
      } else {
        m_pairedColors[1] = l_color;
        m_pairedColorsDist[1] = l_d;
        
        m_nbOfPairedColors++;
        return toReturn;        
      }
    }
    
    // If array is full and point worse than last element
    if (m_nbOfPairedColors == m_size && l_d >= m_pairedColorsDist[m_size - 1]) {
      return null;
    }
    
    // Any other cases find where to insert the element
    int low = 0, high = m_nbOfPairedColors;
    if(l_d < m_pairedColorsDist[0]) {
      low = -1;
      high = -1;
    }
    while (high - low > 1) {
      int mid = (high + low) / 2;
      if (l_d < m_pairedColorsDist[mid]) {
        high = mid;
      } else if (l_d > m_pairedColorsDist[mid]) {
        low = mid;
      } else {
        low = mid;
        high = mid;
      }
    }
    
    // Shift worst elements
    int start = min(m_nbOfPairedColors, m_size - 1);
    for (int i = start; i > low + 1; i--) {
      m_pairedColors[i] = m_pairedColors[i-1];
      m_pairedColorsDist[i] = m_pairedColorsDist[i-1];
    }
    
    // Add new element
    m_pairedColors[low + 1] = l_color;
    m_pairedColorsDist[low + 1] = l_d;
    if (m_nbOfPairedColors < m_size) m_nbOfPairedColors++;
      
    return toReturn;
  }
  
  /*
  * Print the paired colors array with the associated distances
  */
  void prettyPrint(String l_indent) {
    for (int i = 0; i < m_size; i++) {
      if (m_pairedColors[i] == null) {
        println(l_indent + "null");
      } else {
        m_pairedColors[i].prettyPrint();
        println(l_indent + "     " + m_pairedColorsDist[i]);
      }
    }
  }
}


/*
* Class representing a node of the tree.
* Each node represent a color in the target image.
*/
class Node {
  Node m_parent, m_left, m_right;
  PixelColor m_color;
  int m_depth;
  PixelPairings m_pixelPairings;

  /*
  * Class constructor. Only publicly available constructor.
  * Construct all the node of the tree from the list of colors.
  *
  * @param  l_colors     The list of colors where the tree needs to split
  * @param  l_targetColorNb   A hashmap containing the number of pixels of a given color (the key) in the target image
  */
  public Node(PixelColor[][] l_colors, HashMap<Integer, Integer> l_targetColorNb) {
    m_parent = null;
    m_color = l_colors[0][l_colors[0].length / 2];
    m_pixelPairings = new PixelPairings(l_targetColorNb.get(m_color.getInt()));
    m_depth = 0;
    createChildren(l_colors, l_targetColorNb, 0, l_colors[0].length);
  }

  /*
  * Class constructor used in the tree construction to add a children node and continue the recursion (left and right children to be determined by recursion)
  */
  private Node(PixelColor[][] l_colors, HashMap<Integer, Integer> l_targetColorNb, Node l_parent, int l_depth, PixelColor l_nodeColor, int l_from, int l_to) {
    m_parent = l_parent;
    m_color = l_nodeColor;
    m_pixelPairings = new PixelPairings(l_targetColorNb.get(m_color.getInt()));
    m_depth = l_depth;
    createChildren(l_colors, l_targetColorNb, l_from, l_to);
  }

  /*
  * Class constructor used in the tree construction to add a children node and end the recursion (left and right children are null).
  */
  private Node(HashMap<Integer, Integer> l_targetColorNb, Node l_parent, int l_depth, PixelColor l_nodeColor) {
    m_parent = l_parent;
    m_color = l_nodeColor;
    m_pixelPairings = new PixelPairings(l_targetColorNb.get(m_color.getInt()));
    m_depth = l_depth;
    m_left = null;
    m_right = null;
  }

  /*
  * Brain of the tree construction.
  * Creates the nodes by recursively dividing the space in two, looping through the 3 R, G and B directions
  * It uses an implementation of the following paper: https://arxiv.org/pdf/1410.5420.pdf
  */
  private void createChildren(PixelColor[][] l_colors, HashMap<Integer, Integer> l_targetColorNb, int l_from, int l_to) {     
    //If there is no more node to create we can stop the recursion
    if (l_to - l_from == 1) {
      m_left = null;
      m_right = null;
      return;
    }
    
    // If there is only one node left to create we can stop the recursion. The solution is trivial.
    if (l_to - l_from == 2) {
      m_left = new Node(l_targetColorNb, this, m_depth + 1, l_colors[0][l_from]);
      m_right = null;
      return;
    }
    
    // If there is only 2 nodes left to create we can stop the recursion. The solution is trivial.
    if (l_to - l_from == 3) {
      m_left = new Node(l_targetColorNb, this, m_depth + 1, l_colors[0][l_from]);
      m_right = new Node(l_targetColorNb, this, m_depth + 1, l_colors[0][l_to - 1]);
      return;
    }
    
    //Init helper variables
    int midPt = (l_to + l_from) / 2;    
    PixelColor midPixelColor = l_colors[0][midPt];
    int lowerIdx, higherIdx;

    //Copy first column to last column
    for (int i = l_from; i < l_to; i++) {
      l_colors[3][i] = l_colors[0][i];
    }
    l_colors[3][midPt] = null;

    //Sort 2nd column in the 2 halves of the first column according to current direction
    lowerIdx = l_from;
    higherIdx = midPt + 1;
    for (int i = l_from; i < l_to; i++) {
      int comparator = l_colors[1][i].compareTo(midPixelColor, m_depth % 3);
      if (comparator > 0) {
        l_colors[0][higherIdx] = l_colors[1][i];
        higherIdx++;
      } else if (comparator < 0) {
        l_colors[0][lowerIdx] = l_colors[1][i];
        lowerIdx++;
      }
    }
    l_colors[0][midPt] = null;

    //Sort 3rd column in the 2 halves of the second column according to current direction
    lowerIdx = l_from;
    higherIdx = midPt + 1;
    for (int i = l_from; i < l_to; i++) {
      int comparator = l_colors[2][i].compareTo(midPixelColor, m_depth % 3);
      if (comparator > 0) {
        l_colors[1][higherIdx] = l_colors[2][i];
        higherIdx++;
      } else if (comparator < 0) {
        l_colors[1][lowerIdx] = l_colors[2][i];
        lowerIdx++;
      }
    }
    l_colors[1][midPt] = null;

    //Copy last column to Third column
    for (int i = l_from; i < l_to; i++) {
      l_colors[2][i] = l_colors[3][i];
    }
    
    //Recursion
    m_left = new Node(l_colors, l_targetColorNb, this, m_depth + 1, l_colors[0][(l_from + midPt) / 2], l_from, midPt);
    m_right = new Node(l_colors, l_targetColorNb, this, m_depth + 1, l_colors[0][(midPt + 1 + l_to) / 2], midPt + 1, l_to);
  }

  /*
  * Print the color of each node using the specified string as an indent
  */
  public void prettyPrint(String l_indent) {
    print(l_indent);
    m_color.prettyPrint();
    println("");
    
    if(m_left != null) m_left.prettyPrint("  " + l_indent);
    if(m_right != null) m_right.prettyPrint("  " + l_indent);
  }
}


/*
* This class is actually a KDTree with D = 3. The 3 dimensions being the R, G and B channels of a color space.
* Each unique color of a target image is used to create the nodes of the tree.
* The tree is build from the idea from the following paper: https://arxiv.org/pdf/1410.5420.pdf
*/
class PixelProcessor {
  private Node m_root;
  private Node m_nearest;
  private int m_bestDist;
  private PImage m_target;

  /*
  * Class constructor.
  */
  public PixelProcessor(PImage l_targetImg) {
    m_target = l_targetImg;
    createTree(l_targetImg);
    println("Tree built");
    addTargetPixels(l_targetImg);
    println("Target pixels coordinates added");
  }

  /*
  * Create the tree from a target image.
  * First the doubles are removed.
  * Then the colors are sorted by 3 differents super keys RGB, GBR and BRG. => It will allow the tree to be balanced
  * Finally the root node is created and the recursion begin.
  *
  * @param  l_targetImg   The target image containing the pixel to be used for creating the tree
  */
  private void createTree(PImage l_targetImg) {
    HashMap<Integer, Integer> targetColorNb = new HashMap<Integer, Integer>((int)(1 + (l_targetImg.width * l_targetImg.height) / 0.75));

    //Get unique RGB values
    l_targetImg.loadPixels();
    for (int i = 0; i < l_targetImg.width; i++) {
      for (int j = 0; j < l_targetImg.height; j++) {
        int idx = j * l_targetImg.width + i;
        Integer key = (0xFFFFFF & l_targetImg.pixels[idx]);

        if (targetColorNb.containsKey(key)) {
          Integer value = targetColorNb.get(key) + 1;
          targetColorNb.put(key, value);
        } else {
          targetColorNb.put(key, Integer.valueOf(1));
        }
      }
    }

    //Initialize preSortedPixels with the unique RGB value
    PixelColor[][] preSortedPixels;
    preSortedPixels = new PixelColor[4][targetColorNb.size()];

    int idx = 0;
    for (Integer key : targetColorNb.keySet()) {
      preSortedPixels[0][idx] = new PixelColor(key);
      preSortedPixels[1][idx] = new PixelColor(key);
      preSortedPixels[2][idx] = new PixelColor(key);
      idx++;
    }

    //Sort first column with super key RGB
    Arrays.sort(preSortedPixels[0], new Comparator<PixelColor>()
    {
      public int compare(PixelColor o1, PixelColor o2)
      {
        return o1.RGBCompareTo(o2);
      }
    }
    ); 

    //Sort second column with super key GBR
    Arrays.sort(preSortedPixels[1], new Comparator<PixelColor>()
    {
      public int compare(PixelColor o1, PixelColor o2)
      {
        return o1.GBRCompareTo(o2);
      }
    }
    ); 

    //Sort third column with super key BRG
    Arrays.sort(preSortedPixels[2], new Comparator<PixelColor>()
    {
      public int compare(PixelColor o1, PixelColor o2)
      {
        return o1.BRGCompareTo(o2);
      }
    }
    );

    m_root = new Node(preSortedPixels, targetColorNb);
  }

  /*
  * Initialize the pixelPairings of each node with the list of pixels from the target image that share the exact same color as the node.
  *
  * @param  l_targetImg   The target image containing the pixels to be used for creating the tree
  */
  private void addTargetPixels(PImage l_targetImg) {
    //Add target pixels on proper nodes
    for (int i = 0; i < l_targetImg.width; i++) {
      for (int j = 0; j < l_targetImg.height; j++) {
        int idx = j * l_targetImg.width + i;
        PixelColor pixColor = new PixelColor(l_targetImg.pixels[idx]);
        PixelCoord coord = new PixelCoord(i, j);
        addTargetPixel(pixColor, coord);
      }
    }
  }

  /*
  * Select the pixels from the target image to be removed based on the proximity of the pixel colors from a source image.
  *
  * @param  l_sourceImg   The source image containing the pixels to be used for removing the pixels in the target image
  * @param  l_threshold   Distance squared at which a pixel from the source image can be paired to a pixel from the target image
  */
  public PImage removePixelWith(PImage l_sourceImg, int l_threshold) {
    findMatchingPixels(l_sourceImg, l_threshold);
    println("Source image pixels matched");
    
    PImage result = createImage(m_target.width, m_target.height, RGB);
    result.copy(m_target, 0, 0, m_target.width, m_target.height, 0, 0, m_target.width, m_target.height);
    removePixelsOf(result);
    println("Result image updated");
    
    return result;
  }

  /*
  * Find to which target pixel the source pixels should be paired
  *
  * @param  l_sourceImg   The source image containing the pixels to be used for removing the pixels in the target image
  * @param  l_threshold   Distance squared at which a pixel from the source image can be paired to a pixel from the target image
  */
  private void findMatchingPixels(PImage l_sourceImg, int l_threshold) {
    int previousTime = millis();
    l_sourceImg.loadPixels();
    for (int j = 0; j < l_sourceImg.height; j++) {
      for (int i = 0; i < l_sourceImg.width; i++) {
        int idx = j * l_sourceImg.width + i;
        PixelColor pixColor = new PixelColor(l_sourceImg.pixels[idx]);
        findBestMatch(pixColor, l_threshold);
        
        int now = millis();
        if (now - previousTime > 5000) {
          println("Source pixels analyzed: " + idx + " / " + l_sourceImg.height * l_sourceImg.width);
          previousTime = now;
        }      
      }
    }
  }

  /*
  * Find to which target pixel one given source pixel should be paired.
  *
  * @param  l_color       The source image pixel to be paired to a target image pixel
  * @param  l_threshold   Distance squared at which a pixel from the source image can be paired to a pixel from the target image
  */
  public void findBestMatch(PixelColor l_color, int l_threshold) {
    m_nearest = null;
    m_bestDist = 50000000; // Max dist is 3 * 255 * 255 * 255. It needs to be higher.
    bestMatch(m_root, l_color);

    if (m_nearest == null) return;

    if (m_nearest.m_color.distFrom(l_color) > l_threshold) {
      return;
    }

    PixelColor toReinsert = m_nearest.m_pixelPairings.addSourcePoint(l_color, m_bestDist);

    if (toReinsert == null) return;
    findBestMatch(toReinsert, l_threshold);
  }

  /*
  * Find to which target pixel one given source pixel should be paired.
  * Recursion part of the findBestMatch method
  *
  * @param  l_node        The node to investigate
  * @param  l_color       The source image pixel to be paired to a target image pixel
  * @param  l_threshold   Distance squared at which a pixel from the source image can be paired to a pixel from the target image
  */
  public void bestMatch(Node l_node, PixelColor l_color) {
    if (l_node == null) 
      return;

    int d = l_node.m_color.distFrom(l_color);
    if (d < m_bestDist) {
      if (l_node.m_pixelPairings.isBetterMatch(d)) {
        m_bestDist = d;
        m_nearest = l_node;
      }
    }

    if (m_bestDist == 0) 
      return;

    int pivotSide = l_color.compareTo(l_node.m_color, l_node.m_depth % 3);    
    bestMatch( (pivotSide < 0) ? l_node.m_left : l_node.m_right, l_color);

    if (l_color.distFromPlane(l_node.m_color, l_node.m_depth % 3) < m_bestDist) 
      bestMatch( (pivotSide < 0) ? l_node.m_right : l_node.m_left, l_color);
  }

  /*
  * Add a target pixel coordinate to the pixelParings of the proper node that have the exact same color as the target pixel.
  *
  * @param  l_color   The color of the target pixel to add
  * @param  l_coord   The coordinate of the target pixel to add
  */
  public void addTargetPixel(PixelColor l_color, PixelCoord l_coord) {
    addTargetPixelRecursion(l_color, l_coord, m_root);
  }

  /*
  * Add a target pixel coordinate to the pixelParings of the proper node that have the exact same color as the target pixel.
  * Recursion part of the addTargetPixel method.
  *
  * @param  l_color   The color of the target pixel to add
  * @param  l_coord   The coordinate of the target pixel to add
  * @param  l_node    The node to investigate
  */
  private void addTargetPixelRecursion(PixelColor l_t, PixelCoord l_coord, Node l_node) {
    if (l_t.isEqualTo(l_node.m_color)) {
      l_node.m_pixelPairings.addTargetPixelCoord(l_coord);
      return;
    }

    int pivotSide = l_t.compareTo(l_node.m_color, l_node.m_depth % 3);
    addTargetPixelRecursion(l_t, l_coord, (pivotSide < 0) ? l_node.m_left : l_node.m_right);
  }

  /*
  * Remove (turn to white) the pixels of the target image base on the pixel pairing done with the source image.
  *
  * @param  l_resultImg   The result image (same as the target image) on which to remove the pixels
  */
  public void removePixelsOf(PImage l_resultImg) {
    l_resultImg.loadPixels();
    removePixelsOfRecursion(l_resultImg.pixels, l_resultImg.width, m_root);
    l_resultImg.updatePixels();
  }

  /*
  * Remove (turn to white) the pixels of the target image base on the pixel pairing done with the source image.
  * Recursion part of the removePixelsOf method.
  *
  * @param  l_p          The pixel array of the result image (same as the target image) on which to remove the pixels
  * @param  l_imgWidth   The width of the result image
  * @param  l_node       The node to investigate
  */
  private void removePixelsOfRecursion(int[] l_p, int l_imgWidth, Node l_node) {
    if (l_node == null) return;

    PixelCoord[] coord = l_node.m_pixelPairings.m_targetCoords;

    for (int i = 0; i < l_node.m_pixelPairings.m_nbOfPairedColors; i++) {
      int idx = coord[i].y * l_imgWidth + coord[i].x;
      l_p[idx] = color(255);
    }

    removePixelsOfRecursion(l_p, l_imgWidth, l_node.m_left);
    removePixelsOfRecursion(l_p, l_imgWidth, l_node.m_right);
  }

  /*
  * Print the tree nodes in the console with indents
  */
  void prettyPrint() {
    m_root.prettyPrint("+- ");
  }
}

EDIT:
I think the performance could be improved even more if the source pixels are added in parallel. In that case, the pairing would be performed in several passes.
For the first pass, the source pixels are paired to the closest target pixel without taking care of the number of spots available (this can be done in parallel). If, at some nodes, more source pixels are paired that there are available target pixels, the furthest source pixels are removed and put back into the backlog of source pixels to be handled.
For the next pass, the source pixels in the backlog are added in parallel again but this time the “full” nodes are ignored. Again the excess capacity is put back into the source pixel backlog.
And so on, and so on…