Search code examples
algorithmoptimizationcolorsmathematical-optimizationnonlinear-optimization

Better algorithm with CIEDE2000 to find true opposite colors


Using Python 3.7 and the colormath module, I had some fun trying to find the complete opposite of one color (ex. The opposite color of black [0, 0, 0] is yellow [255, 255, 0] with a DE of 101.20397657762743) or trying to find the two colors with the most color difference (i.e. "Navy" [0, 0, 110] and "Chartreuse" [143, 255, 0] with a DE of 119.4740815993416, from my potentially inaccurate testing).

Unfortunately, the only way I have found to find the opposite of a given color is just to bruteforce with a little bit of optimization by comparing the given color with (almost) every single sRGB color combo (or, [0, 0, 0] to [255, 255, 255]).

Python 3.7 code:

from colormath.color_objects import sRGBColor as RGBC, LabColor as LabC
from colormath.color_conversions import convert_color as cv_c
from colormath.color_diff import delta_e_cie2000 as de2k
from time import perf_counter as clock

# Example: X11's "Midnight Blue", edit as 'RGBC(r, g, b,...'
reference = RGBC(25, 25, 112, is_upscaled=1)
lab_ref = cv_c(reference, LabC)

# Set max delta
max_delta = 0

# Set estimate calculation's range inverals as factors of 255 (1, 3, 5, 15, 17, 51, 85, 255)
resolution = 17

# Estimate the opposite color
for sample_r in range(0, 256, resolution):
    for sample_g in range(0, 256, resolution):
        for sample_b in range(0, 256, resolution):
        
            # Convert the sample to Lab
            sample = RGBC(sample_r, sample_g, sample_b, is_upscaled=1)
            lab_samp = cv_c(sample, LabC)
            
            # Find the color difference
            delta = de2k(lab_ref, lab_samp)
            
            # Compare current delta with previous highest delta
            if delta > max_delta:
                # Overwrite highest delta with current delta
                max_delta = delta
                
                # Save estimate for optimization
                estimate = sample.get_upscaled_value_tuple()

# Keep any 255 values from estimate, leads to 'range(255, 256)'
low_r = 0 if estimate[0] != 255 else 255
low_g = 0 if estimate[1] != 255 else 255
low_b = 0 if estimate[2] != 255 else 255

# Keep any 0 values from estimate, leads to 'range(0, 1)'
high_r = 256 if estimate[0] != 0 else 1
high_g = 256 if estimate[1] != 0 else 1
high_b = 256 if estimate[2] != 0 else 1

# Reset max delta
max_delta = 0

# Find a better opposite color from estimate
for sample_r in range(low_r, high_r):
    for sample_g in range(low_g, high_g):
        for sample_b in range(low_b, high_b):
            
            # Convert the sample color to Lab
            sample = RGBC(sample_r, sample_g, sample_b, is_upscaled=1)
            lab_samp = cv_c(sample, LabC)
            
            # Find the color difference
            delta = de2k(lab_ref, lab_samp)
            
            # Compare current delta with previous highest delta
            if delta > max_delta:
                # Overwrite highest delta with current delta
                max_delta = delta

                # Overwrite best opposite color with current sample
                opposite = sample

# Print the reference, opposite color, and delta E
print(f'{reference.get_upscaled_value_tuple()} with {opposite.get_upscaled_value_tuple()}, {max_delta}')

# Print program time
print(clock())

# Example: 
# Returns '(25, 25, 112) with (166, 255, 0), 108.95350620860522
#          2.580066949'

The above code will run for 2.5 seconds to about 19 minutes (assumes second opposite calculation takes 128^3 jumps, returns same value) to about 2 hours 15 minutes (assumes worst possible case is 254^3 jumps, as in no optimization occurs) on my system (3.00 GHz)

CIEDE2000's equations can be seen at this site. Notice the notes at the bottom of the page.

I want to know if there is a more efficient algorithm that can find the complete opposite of a given RGB color. I want to be able to use said algorithm to create a spreadsheet that contains every RGB color and its opposite color before CIEDE202X or CIEDE20XX comes out.

Note: this is my first question on StackOverflow

Edit 1: Created better estimate optimization. Instead of just keeping 0 or 255 if the estimate had one, I would reduce the limits so that they were only at a maximum of +-32 from the estimate. Runtime reduced to at most 4 seconds on 3.00 GHz.

# Red channel optimization
if estimate[0] == 0 or estimate[0] == 255:
    low_r = estimate[0]
    high_r = estimate[0] + 1
elif estimate[0] > 32 and estimate[0] < 224:
    low_r = estimate[0] - 32
    high_r = estimate[0] + 32
elif estimate[0] < 33:
    low_r = 0
    high_r = estimate[0] + 32
else:
    low_r = estimate[0] - 32
    high_r = 256

# Green channel optimization
if estimate[1] == 0 or estimate[1] == 255:
    low_g = estimate[1]
    high_g = estimate[1] + 1
elif estimate[1] > 32 and estimate[1] < 224:
    low_g = estimate[1] - 32
    high_g = estimate[1] + 32
elif estimate[1] < 33:
    low_g = 0
    high_g = estimate[1] + 32
else:
    low_g = estimate[1] - 32
    high_g = 256

# Blue channel optimization
if estimate[2] == 0 or estimate[2] == 255:
    low_b = estimate[2]
    high_b = estimate[2] + 1
elif estimate[2] > 32 and estimate[2] < 224:
    low_b = estimate[2] - 32
    high_b = estimate[2] + 32
elif estimate[2] < 33:
    low_b = 0
    high_b = estimate[2] + 32
else:
    low_b = estimate[2] - 32
    high_b = 256

Solution

  • CIE DeltaE 2000 is not appropriate for large colour differences so large differences in the range [10, 20]+ simply cannot be evaluated with this quasi-metric and you should probably look at something else such as HyAB colour difference metric: https://onlinelibrary.wiley.com/doi/abs/10.1002/col.22451 or alike.