Search code examples
pythonimage-processingphotoshopphotoshop-script

Counting coloured swatches - faster method required


I'm currently trying to find a way to count coloured swatches. They are all regular, same size, arranged in a chess board pattern, colours vary. The number of swatches will vary from a few hundred to about 90,000.

swatch sample

In this example, of only three colours, I need it to count the number of light green squares = 8

In Photoshop I can take a sample of each swatch, where x, y are the centre of the swatch, and colArr is an array of all the RGB values of those swatches.

var pointSample = app.activeDocument.colorSamplers.add([x,y]);

// Obtain array of RGB values.
var rgb = [
  Math.round(pointSample.color.rgb.red,0),
  Math.round(pointSample.color.rgb.green,0),
  Math.round(pointSample.color.rgb.blue,0)
];

colArr.push(rgb);
delete_all_colour_samples();

function delete_all_colour_samples()
{
  app.activeDocument.colorSamplers.removeAll();
}

If the colour matches one in a previously established array, it gets counted.

It works! It's fairly instantaneous for small samples. However, with an image of over 3000 samples it takes about 25 seconds. Whilst I'm content to know that the image has been processes, I am curious if it can be done quicker.

My question is this: Could this be sped up by using say Python, but how?


Solution

  • The details of what you are asking are not quite clear to me yet - like whether the squares are always the same size in every picture, whether you know in advance how many there are in a given image, whether they are always square (you describe them as "regular") and whether you actually want to count more than one colour in each image - your example is "light green" but you seem to have an array "previously established colours" so it is not clear if you want to know how many pixels are of each colour in that array.

    Anyway, let's make a first stab at an answer, using OpenCV and basic Numpy indexing to get the centres.

    import cv2
    import numpy as np
    
    # Load an image
    im = cv2.imread('Qcza1.png')
    
    # Assume the swatches are 64x64 pixels
    sw, sh = 64, 64
    
    # Get colours at each swatch centre, so we start 1/2 a swatch in and step by a whole swatch
    centres = im[sh//2::sh, sw//2::sw]
    

    That gives us this array of size [4,6,3] because Numpy arrays are [height, width, channels] and we have 4 swatch centres vertically, 6 swatch centres horizontally and each has 3 colours.

    array([[[141, 244, 159],
        [ 85, 202, 105],
        [255, 255, 255],
        [ 85, 202, 105],
        [141, 244, 159],
        [ 85, 202, 105]],
    
       [[255, 255, 255],
        [141, 244, 159],
        [ 85, 202, 105],
        [141, 244, 159],
        [ 85, 202, 105],
        [141, 244, 159]],
    
       [[141, 244, 159],
        [ 85, 202, 105],
        [255, 255, 255],
        [ 85, 202, 105],
        [141, 244, 159],
        [255, 255, 255]],
    
       [[ 85, 202, 105],
        [141, 244, 159],
        [ 85, 202, 105],
        [255, 255, 255],
        [ 85, 202, 105],
        [255, 255, 255]]], dtype=uint8)
    

    In case you are unfamiliar with Numpy indexing, it is basically:

    array[start:end:stride]
    

    so in the code above I am starting half a swatch width into the array (to get to the centre of the first swatch) and then using a stride equal to the swatch width to get to the next centre.

    If you now want to tally the number of swatch centres having color [141,244,159], you can do:

    tally = np.sum(np.all(centres==[141,244,159], axis=-1))
    

    which gets the result 8.


    Note that you can do this all with Python Imaging Library PIL/Pillow, if that is an easier install for you, by replacing:

    import cv2
    im = cv2.imread(...)
    

    with:

    from PIL import Image
    
    # Open PIL Image and make into Numpy array
    im = np.array(Image.open(...).convert('RGB'))
    

    and bear in mind that OpenCV uses BGR ordering whereas PIL uses RGB, so the colour triplets you are looking for will be reversed.


    I generated a sample image of 90,000 swatches of 10x10 pixels using 1,000 unique colours like this:

    import cv2
    import numpy as np
    
    # Define 10 possible values for R, G and B
    vals = np.arange(0,255,26)
    
    # Make 90,000 pixel image using approx 10*10*10 = 1000 colours
    h, w = 200, 450
    np.random.seed(42)
    im = np.random.choice(vals, (h,w,3)).astype(np.uint8)
    
    # Scale up to make swatches 10x10
    sw, sh = 10, 10
    im = cv2.resize(im, (w*sw, h*sh), 0, 0, interpolation=cv2.INTER_NEAREST)
    

    I then timed it and got 1.9ms and it found 83 swatches with colour [0, 78, 156] which is what you'd expect with 90,000 swatches of 1,000 colours:

    %%timeit
    centres = im[sh//2::sh, sw//2::sw]
    np.sum(np.all(centres==[0,78,156], axis=-1))
    
    1.95 ms ± 30.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)