Python image processing : determine if a image file is all solid color etc.?

Hi All,

I am looking for a way to efficiently check if an image on HD (JPG, PNG, BMP) is all singe solid color (ie. all black, all white, or, if PNG, all transparent). Is there any functionality like that with System.Drawing or similar that could be used to determine that and could be implemented in Python with Rhino ?

thanks for any hints.

–jarek

Hi @Jarek, how large are the images ?

_
c.

Hi @clement,

The size varies, but could be as big as 6000pix each dimension; if size was an issue i could probably make-do with analyzing screen-size images before producing bigger ones…

–jarek

Hi Jarek,

Maybe below helps for a start. I’ve tested with 2K png images, all white, black, blue or transparent but it is not so fast since all bits have to be analyzed. If at least one bit does not match test_color, it stops immediately.

Note: For the comparison it is important to define test_color as Color.FromArgb() and not like Color.White. Please save before trying.

JarekTest.py (1.5 KB)

_
c.

You probably could just get rid of the bln_values and use one boolean you set to False.

if not colcheckhere:
  bln = False # instead of bln_values[i] = False
  state.Break()

return bln # instead of all(bln_values)

Save you allocating a huge list of values that aren’t really used.

Hi @clement

Thanks! I think that is a good start :wink: I am hoping it actually could be a bit simpler, and also faster. Unfortunately I don’t understand the whole code with Worker etc. - but I was playing with it to see if I can modify it. In fact what I need is only to check if the entire PNG is transparent (Alpha=0), so I tried to change the code. It works on images that are fully not-transparent and fully transparent; on mixed ones I get an error.
Also it is quite slow on images >1500px and very slow on 6000px+ (fully transparent ones) - images with no transparency, even larger ones, go a bit faster.

I was trying to apply what @nathanletwory suggested but it would always return “True” no matter what, so probably I was applying it wrong.

Apart from the above, would there be a way to speed this up by either resizing the image in memory to ~1000pix in longer dimension, or just skipping #n rows / columns to read no more than 1000 each way?

Another way I thought of speeding this up was to detect the non-transparent pixels by checking it not from up/left to bottom/right but in some way that samples the images in a grid that gets denser - the files I would be dealing with would have lots of transparency (background) with a cluster/shape of non-transparent pixels here and there…

Here is the version I tried to modify:

ImageTest.py (1.5 KB)

thanks!!

–jarek

Here my modified version, alongside the original version by @clement.

Before you run the script read through it - it is currently set up for comparing the two different implementations by running each 20 times (so 40 times of comparing in total).

Averages look like this: ('clement way: ', 2.9294063568115236, 'jesterKing way: ', 0.84793128967285158)

The speedup is mainly due to not creating colors, nor having a huge boolean array.

The test image (of size 1024x1072) is also attached.

The redacted code is:

def IsImageAllSameColorNew(image_path, color):
    if not os.path.exists(image_path): return
    
    # set up bitmap reading
    bmp = System.Drawing.Bitmap.FromFile(image_path)
    form = System.Drawing.Imaging.PixelFormat.Format32bppArgb
    mode = System.Drawing.Imaging.ImageLockMode.ReadOnly
    area = System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height)
    # and lock the data for specific format so we can copy for consumption
    data = bmp.LockBits(area, mode, form)
    
    # copy the image data to rgb_values array, so we can throw the bitmap away
    byte_count = (data.Stride) * data.Height
    rgb_values = System.Array.CreateInstance(System.Byte, byte_count)
    System.Runtime.InteropServices.Marshal.Copy(data.Scan0, rgb_values, 0, byte_count)
    bmp.UnlockBits(data)
    bmp.Dispose()
    
    # create a single-item list, so we can actually set it
    # it appears that using just same_color = True, then set
    # it in the worker doesn't work as expected. The list works
    # since it is really a reference, not a hard-copy of the value
    same_color = [True]
    
    # our worker takes an item from the input in i, one element
    # created by the xrange. The state we use to break the loop
    # as soon as we find a dissimilar color from what we want
    def Worker(i, state, unused):
        # skip creation of a Color, we can directly compare
        if rgb_values[i:i+4] != color:
            same_color[0] = False
            state.Break()
    
    # do the work
    task.Parallel.ForEach(xrange(0, byte_count, 4), Worker)
    
    return same_color[0]

def DoSomethingNew():

    image_path = r"your image path here"
    
    # create System.Array<Byte> to hold color values
    test_color = System.Array.CreateInstance(System.Byte, 4)
    test_color[0] = 14 #b
    test_color[1] = 201 #g
    test_color[2] = 255 #r
    test_color[3] = 255 #a

    result = IsImageAllSameColorNew(image_path, test_color)

    print "Result:", result

JarekTest.py (4.1 KB)

@Jarek, so, I was bored and did some further optimization of the solid color tester:

# Image solid color checker by Clement
# optimizations by Nathan 'jesterKing' Letwory

import System
import os
import time
from System.Threading import Tasks as task


def IsImageAllSameColor(image_path, color):
    """Read image from path, then test each pixel against the 
    passed in color. Color is a list of color components
    with order [r, g, b, a]
    """

    if not os.path.exists(image_path): return
    
    # unpack the color, since we don't want to waste time on
    # lookups
    r, g, b, a = color
    
    # set up bitmap reading into array
    bmp = System.Drawing.Bitmap.FromFile(image_path)
    form = System.Drawing.Imaging.PixelFormat.Format32bppArgb
    mode = System.Drawing.Imaging.ImageLockMode.ReadOnly
    area = System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height)
    # and lock the data for specific format so we can copy for consumption
    data = bmp.LockBits(area, mode, form)
    
    # copy the image data to rgb_values array, so we can throw the bitmap away
    byte_count = (data.Stride) * data.Height
    rgb_values = System.Array.CreateInstance(System.Byte, byte_count)
    System.Runtime.InteropServices.Marshal.Copy(data.Scan0, rgb_values, 0, byte_count)
    # done with original bmp, unlock and release file handle
    bmp.UnlockBits(data)
    bmp.Dispose()
    
    # create a single-item list, so we can actually set it
    # it appears that using just same_color = True, then set
    # it in the worker doesn't work as expected. The list works
    # since it is really a reference, not a hard-copy of the value
    same_color = [True]
    
    # our worker takes an item from the input in i, one element
    # created by the xrange. The state we use to break the loop
    # as soon as we find a dissimilar color from what we want
    def Worker(i, state, unused):
        # we now test against the r,g,b,a variables in that order. Assuming
        # that alpha channel isn't what we're looking for we can put it last
        # so that any of r,g,b short-circuit. At best the r value is different
        # resulting in skipping of tests for g,b,and a. Worst case is only alpha
        # is different.
        # Also we no longer use any data structure to compare against to get rid
        # of any lookups
        if rgb_values[i+2] != r or rgb_values[i+1] != g or rgb_values[i] != b or rgb_values[i+3] != a:
            same_color[0] = False
            state.Break()
    
    # do the work
    task.Parallel.ForEach(xrange(0, byte_count, 4), Worker)
    
    return same_color[0]

def DoSomethingNew():
    image_path = r"N:/solidcolor_tester.png"
    
    test_color = [255,201,14,255] # r, g, b, a

    start = time.time()
    result = IsImageAllSameColor(image_path, test_color)
    end = time.time()
    
    return result, end-start

r = DoSomethingNew()
print r
#for i in range(5):
#    r = DoSomethingNew()
#    print r

is_image_solid_color.py (2.9 KB)

The results with the same orange tester image are between 0.3 and 0.8 seconds, averaging more between 0.4 and 0.5.

Hi @nathanletwory, i should have let my uncommented code in but i made the mistake of removing it before posting. If you look at the version @Jarek posted, i had exactly in what he commented out. The reason why every pixel had it’s own test result in the array of booleans was that using state.Break() will not ensure that another thread sets this to True, after the break is done. If you read the remarks on the help page it says:

…It effectively cancels any additional iterations of the loop. However, it does not stop any iterations that have already begun execution

I’ve occassionally encountered exactly what is described above. It set the single function boolean to True after it was set to False by another thread leading to an overall wrong function return value. I’ve not tested yet if state.Stop() would be more reliable as i had no time to optimize the code yet despite of making it run in parallel. This may also explain what @jarek encountered after applying it:

So i’ve chosen the thread-safe way of making one boolean per thread. Imho this was not clever memory wise, but i’ve seen no noticable impact on speed with my 2K images.

Absolutely. If you know that testing just a resized image will have the fully transparent or non transparent pixels. You can probably get away faster by analyzing only a portion of the image and you may skip pixels from the test by changing the number 4 in this line:

task.Parallel.ForEach(xrange(0, byte_count, 4), Worker)

If you would change it to 8, only every second pixel will be tested. The number you use must be divideable by 4 and smaller than width or height.

You still create colors by using Marshal.Copy, but making the comparison with bytes is very effective as you skip Color.FromArgb. The immediate Unlock is very good.

Are you 100% sure this solves what i wrote above about state.Break() ? It would be nice.

I see a 4-6 times speedup from the last version you’ve posted compared to my initial one. If @Jarek now skips pixels from the test it should be very fast, even for larger images.

_
c.

Yes, I am confident it works, since there is only one same_color = [True], and we only actually set it to False before breaking - not setting it to True ever after starting the worker threads. So we don’t have to worry about other threads setting the element to True - since that is never happening.

Main point to note is that I did same_color = [True], not same_color = True. The first will works since a variable with a list is really just a reference to that list. That is how we can change the one element with same_color[0] = False.

The original suggestion was to use same_color = True. That won’t work - the list variant does as explained above.

Not really. The code creates an Array and copies data to it. No System.Drawing.Color are created here. In your worker you first read the four color components (4 list accesses), then you create a System.Drawing.Color from those four. Then you do worst case 8 accesses to System.Drawing.Color and at most 4 comparisons - the comparisons is best case 3 operations (two access, one comparison), at worst 12.

My worker only does the 4 list accesses, then 4 comparisons at worst, just one at best.

As said my tester doesn’t create an Array the size of the image width* image height. Further more, once done you use all(the_huge_bool_array), which will be increasingly slower for increasing image sizes. The list-with-one-element I use is evaluated in constant time regardless of image size.

@clement, @nathanletwory, thanks so much - it is all very very helpful!
I think with the new version, additional hints from Clement and some of my tinkering I have now a script that is reasonably fast…

So as I mentioned I only need to check if the image is all transparent, I don’t care about r,g,b values. I changed the code but can’t tell if there is any additional opportunity for speedup if we don’t analyze r,g,b. Is there any other potential optimization considering that?

Here is the latest version:
is_image_all_transparent.py (3.1 KB)

I added a condition to make number of skipped pixels depending on overall image dimensions, which resulted in big speedups too (for my purpose I don’t need to comb through every pixel on 6000px+ images as the areas to detect are larger clusters of pixels, and if they are tiny, they are insignificant and may as well be discarded).

So with the above, the worst-case scenario (slowest to process) are images that are fully transparent since we analyze them all with no break. Now images ~6000px wide would process in 0.4s here skipping 16pixels. This works great for what I needed. Thank you both again for your help!

( @nathanletwory - need you bored more often…)

–jarek

In your worker you now only analyze the alpha component. I don’t readily see any obvious opportunity to further optimize.

As they say in Finland: “It is good to live in hope.”

1 Like

In your newest version you should get a noticable speedup since you only check one of the 4 byes (the one for alpha).This alone makes it much faster i guess. It would be interesting how the function behaves if it is compiled as dll using ipy.exe and clr.CompileModules().

I would also appreciate if @nathanletwory spends more time here beeing bored. But i’ve heard in Finland nobody gets really bored. They’re all busy raking or cleaning the forest :roll_eyes:

_
c.

3 Likes

Don’t worry. I don’t go out that much. I know the top term tree - couldn’t tell you anything else. I’m more into binary trees.

1 Like

Yes, it is fast now and useable for what I needed it to do! Thanks to both of you guys again.

Now, please go back to raking and vacuming…

1 Like