Image Processing, Software

Python Color Scope

Doing color correction in a program like Divinci Resolve we can see these interesting plots based on your loaded image, like the one below.

These are called color scopes, they are primarily used to identify the pixel colors as you traverse the image from left to right. They are useful for photographers and videographers to help correct the final colors you want in the image. This is because the scope displays the true pixel color intensity instead of relying on your own machine monitor render.

One example would be if you wanted to make sure your colors stayed in range for a particular broadcast medium that could not handle higher color intensities. You can learn more about their use from this blog post below that helped me understand them better.

Color Scope Algorithm

So I decided to try my hand at creating some of these plots using the OpenCV, NumPy, and Matplotlib. I am using Jupyter Notebooks again to test and bring up the algorithm.

Really all we need to do is loop over the image and map each pixel intensity to the vertical scale as a hit map, increasing the value as more pixels fall into that pixel intensity, as we move left to right.

#Slow Algorithm
for w in range(img_width):
    for h in range(img_height):
        pix_val = -(img[h][w]-255)
        if color_scope[pix_val][int(w/div)] != 255:
            color_scope[pix_val][int(w/div)] = \
                color_scope[pix_val[int(w/div)] + 1

The issue with this is that python is too slow when it is working with high-resolution images. This implementation on the test image took 121.55 seconds, 2 minutes!!! This is because the python loops and pixel indexing are not well optimized. For that, we need to rework out an algorithm to use faster matrix operations built into NumPy.

If we look at the data assembled in the plot, all it is doing is finding all the unique pixel values and marking how many times they show up in a particular column. Thankfully there is a NumPy function called “unique” which will return the unique values and their frequency based on a given input matrix. Now all we need to do is loop over the downsampled columns of the image and use this function to map the data onto the color scope plot.

#Fast Algorithm
for l in range(scope_len):
    vals, cnts = np.unique(img[:,l*div:(l+1)*div], return_counts=True)
    for i in range(len(vals)):
        if cnts[i] < 255:
            bw_color_scope[-(vals[i]-255)][l] = cnts[i]
        else:
            bw_color_scope[-(vals[i]-255)][l] = 255

The new implementation took 1.375 seconds on the same image!!! Wow, that was a significant speedup! There is definitely a lesson here in use the optimized functions wherever possible. But even this speedup is nowhere near the fully optimized version that you can see in the Divinci Resolve software, which is amazingly free to use!

Results

I used one of my images that came from my Leica M8 and was used for all the color scopes in this blog post including the example from Divinci Resolve.

Original Image

I used the fast algorithm to produce scope plots for a black and white image as well as separated color channels like the color parade and waveform options in Divinci Resolve.

Black and White Scope
Full Waveform Color Scope

Jupyter Notebook

You can find the whole algorithm in my Jupyter Notebook that I created to produce all the scope plots seen above in the results.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

If you want to run the code, you can clone my GitHub repo with the notebook and the test image that I used.

GitHub: Python Color Scopes

Conclusion

This was a fun exercise, as I was always curious how these graphs were created and their true purpose. They are quite intimidating at first but once you understand them they become much more useful in the future, enabling you to better understand your images and how best to modify them to produce color-accurate results.