Home > Software engineering >  If given a color code RGB [0.52,0.62,0.24] using one, two or three closest color codes how can i obt
If given a color code RGB [0.52,0.62,0.24] using one, two or three closest color codes how can i obt

Time:09-07

I first created a dictionary of 21 different color codes with their names

rgb_colors = {"Red":[1.0,0.0,0.0],"Green":[0.0,1.0,0.0],"Blue":[0.0,0.0,1.0],
             "Black":[0.0,0.0,0.0],"Almond":[0.94,0.87,0.8],"White":[1.0,1.0,1.0],
            "Brown":[0.8,0.5,0.2],"Cadet":[0.33,0.41,0.47],"Camel":[0.76,0.6,0.42],
            "Capri":[0.0,0.75,1.0],"Cardinal":[0.77,0.12,0.23],"Ceil":[0.57,0.63,0.81],
            "Celadon":[0.67,0.88,0.69],"Champagne":[0.97,0.91,0.81],"Charcoal":[0.21,0.27,0.31],
            "Cream":[1.0,0.99,0.82],"Cyan":[0.0,1.0,1.0],"DarkBlue":[0.0,0.0,0.55],
            "AmericanRose":[1.0,0.01,0.24],"Gray":[0.5,0.5,0.5],"Wenge":[0.39,0.33,0.32]}

Then I converted it to Df

RGB = pd.DataFrame(rgb_colors.items(), columns = ["Color","Color Code"])

Then I created a list of all the color codes and asked for input code. then I used the input color and and found the Euclidean distance between each color code to the input and asset a threshold to select the code that matches at least 60% and used the top three codes as the closest colour.

#list of colors
list_of_rgb = [[1.0,0.0,0.0],[0.0,1.0,0.0],[0.0,0.0,1.0],[0.0,0.0,0.0],[0.94,0.87,0.8],
                 [1.0,1.0,1.0],[0.8,0.5,0.2],[0.33,0.41,0.47],[0.76,0.6,0.42],
                  [0.0,0.75,1.0],[0.77,0.12,0.23],[0.57,0.63,0.81],
                  [0.67,0.88,0.69],[0.97,0.91,0.81],[0.21,0.27,0.31],
                  [1.0,0.99,0.82],[0.0,1.0,1.0],[0.0,0.0,0.55],[1.0,0.01,0.24]
                  ,[0.5,0.5,0.5],[0.39,0.33,0.32]]
#input color
print("Enter R,G,B color codes")
color1 = []
for i in range(0,3):
    ele = float(input())
    color1.append(ele)
      
print(color1)

def closest(colors,color, threshold=60, max_return=3):
    colors = np.array(colors)
    color = np.array(color)
    distances = np.sqrt(np.sum((colors-color)**2,axis=1))
    boolean_masks = distances < (1.0 - (threshold / 100))
    outputs = colors[boolean_masks]
    output_distances = distances[boolean_masks]
    return outputs[np.argsort(output_distances)][:max_return]

closest_color = closest(list_of_rgb, color1)

closest_color

suppose the Input is [0.52,0.5,0.5] then closest colors are

array([[0.5 , 0.5 , 0.5 ],
       [0.76, 0.6 , 0.42],
       [0.8 , 0.5 , 0.2 ]])

My question is, how can I find how much percentage of each of these closest color should be used to get the input color?

It can be solved by finding 3 proportions p1,p2 and p3 such that p1 p2 p3=1 and

p1*(r1,g1,b1) p2*(r2,g2,b2) p3*(r3,g3,b3) = (r0,g0,b0)

I'm unable to find p1,p2 and p3. Can anyone help me out on how can I find the p values?

CodePudding user response:

You can make a system of equations to come up with a weighting vector which will tell you the combination of the three colors which exactly equals the input. The form of this is Ax=b, where A is the color matrix, x are the unknown variables to solve, and b is the color target. You have already calculated the 'A' in this situation, it just needs to be transposed. Of course, you also have your target color. However, this is not mapped to the fuzzy set (i.e. from 0 to 1 inclusive). If, for instance, you can only vary the intensity (from 0 to 1 or equivalently 0% to 100%) of the three colors to achieve this input color, then this approach is not sufficient. If you need each of the weighting values to be between 0 and 1, then you can solve a linear program in which you specify the constraints of 0<=w<=1 on the weights. That seems a little complicated for this, but it can be done if that's an interest.

Edit: I added the linear program to solve the problem. Linear programs are used to solve complex optimization problems where there are constraints that are placed on the variables in the system of equations. They are very powerful and can accomplish a lot. Unfortunately, it raises the complexity of the code quite a bit. Also, just to let you know, there is no guarantee that there will be a solution in which all the variables are in the set [0,1]. I believe in this particular example, it is not possible, but it does get very close.

import numpy as np

# target vector
input_color = np.array([.52, .5, .5])
input_color = np.reshape(input_color, (1, len(input_color))).T

# create color matrix with 3 chosen colors
color_1 = np.array([.5, .5, .5])
color_2 = np.array([.76, .6, .42])
color_3 = np.array([.8, .5, .2])
C = np.vstack([color_1, color_2, color_3]).T

# use linear algebra to solve for variables
weights = np.matmul(np.linalg.pinv(C),input_color)

# show that the correct values for each color were calculated
print(weights[0]*color_1   weights[1]*color_2   weights[2]*color_3)




from scipy.optimize import linprog

color_1 = np.array([.5, .5, .5])
color_2 = np.array([.76, .6, .42])
color_3 = np.array([.8, .5, .2])

# make variables greater than zero
ineq_1 = np.array([-1, 0, 0])
ineq_2 = np.array([0, -1, 0])
ineq_3 = np.array([0, 0, -1])

# make variables less than or equal to one
ineq_4 = np.array([1, 0, 0])
ineq_5 = np.array([0, 1, 0])
ineq_6 = np.array([0, 0, 1])

C = np.vstack([color_1, color_2, color_3]).T
C = np.vstack([C, ineq_1, ineq_2, ineq_3, ineq_4, ineq_5, ineq_6])

A = C

input_color = np.array([.52, .5, .5])
b = np.concatenate((input_color, np.array([0, 0, 0, 1, 1, 1])),axis=0)
b = np.reshape(b, (1, len(b))).T

# scipy minimizes, so maximize by multiplying by -1
c = -1*np.array([1, 1, 1])


# Visually, what we have right now is

# maximize f = x1   x2   x3
#    color_1_red*x1   color_2_red*x2   color_3_red*x3 <= input_color_red
#    color_1_gre*x1   color_2_gre*x2   color_3_gre*x3 <= input_color_gre
#    color_1_blu*x1   color_2_blu*x2   color_3_blu*x3 <= input_color_blu
#    x1 >= 0
#    x2 >= 0
#    x3 >= 0
#    x1 <= 1
#    x2 <= 1
#    x3 <= 1

# As you'll notice, we have the original system of equations in our constraint
#  on the system. However, we have added the constraints that the variables
#  must be in the set [0,1]. We maximize the variables because linear programs
#  are made simpler when the system of equations are less than or equal to.


# calculate optimal variables with constraints
res = linprog(c, A_ub=A, b_ub=b)

print(res.x)

print(res.x[0]*color_1   res.x[1]*color_2   res.x[2]*color_3)

CodePudding user response:

The system of linear equations you are setting up is over-determined, meaning that in general there is no solution. The additional constraints on the proportions (summing up to 1, being in the [0, 1] range) make things worse because even in case a solution exists, it may be discarded because of those additional constraints.

The question in its current form is not mathematically solvable.

Additionally, the combination of colors the way you are suggesting, i.e. a weighted sum, is likely not what you want. Most probably, you want a weighted average instead.

For example, if you were to use a weighted sum, the [0, 0, 0] would never contribute to forming the new color. The only effect it may have is to contribute to the requirement on the sum of the proportions without contributing to the color itself.

Likewise, the requirement for the proportions to sum up to something fixed brings too much restriction into which colors can be represented, and this is probably dictated by the idea that they should be proportions, so it would be a sane requirement for them to sum up to something (ideally 1). However, in light of using weighted average, what you call proportions are actually weights and the requirements for them to be adding up to something makes less sense.

Whether you want weighted sum, or weighted average, and you want to include a fixed sum constraints or not, the mathematics is very similar and although exact solutions are not always attainable, it is possible get to approximate solutions.

One way of computing the is through linear programming, which gets you essentially to @greenerpastures's answer, but requires you to use linear programming black boxes.


Here I propose a more basic approach where only simple linear algebra is involved, but ignores the requirement for weights being in the [0, 1] range.

The equations for writing a target color b as a linear combination of colors can be written in matrix form as:

A x - b = 0

with A formed by the colors you want to use, b is the target color and x are the weights.

/ r0 r1 r2 \                / r_ \   / 0 \
| g0 g1 g2 | (x0, x1, x2) - | g_ | = | 0 |
\ b0 b1 b2 /                \ b_ /   \ 0 /

Now, this system of equations admits a single solution if det(A) != 0. Since among the selected colors there is an ortho-normal basis, you can actually use those to construct an A with det(A) != 0, and hence a x can always be found. If the elements of b are in the [0, 1] range, so are the elements of x, because essentially b = x.

In general, you can find the solution of the Ax - b = 0 linear system of equations with np.linalg.solve(), which can be used to look for x when A is formed by other colors, as long as det(A) != 0.

If you want to include more or less than as many colors as the number of channels, then approximate solutions minimizing the sum of squares can be obtained with np.linalg.lstsq().

Once you are set to find approximate solution, the requirement on the sum of the weights becomes an additional parameter in the system of linear equation.

This can be included by simply augmenting A and b with an extra dimension set to 1 for A and to q for b, so that A x - b = 0 becomes:

/ r0 r1 r2 \                / r3 \   / 0 \
| g0 g1 g2 | (p0, p1, p2) - | g3 | = | 0 |
| b0 b1 b2 |                | b3 |   | 0 |
\  1  1  1 /                \  q /   \ 0 /

Now the new equation p0 p1 p2 = q is included.

Likewise, if we want to model a weighted average rather than a weighted sum, we need to replace b with k * b where k is the number of columns of A, so that the k value appear in the denominator of the weighted sum, making it an average.

While all this can work with arbitrary colors, the ones selected by closeness are not necessarily going to be good candidates to approximate well an arbitrary color.

For example, if the target color is (1, 0, 1) and the 3 closest colors happen to be proportional to each other, say (0.9, 0, 0), (0.8, 0, 0), (0.7, 0, 0), it may be better to use say (0, 0, 0.5) which is farther but can contribute better to make a good approximation than say (0.7, 0, 0).


Now back to the problem, we now have some means of finding which linear combination of colors will approximate best a given target color.

With this, given that the numbers are fairly small, it is possible to investigate with brute-force which combination of num colors out of all fixed colors, will produce the best approximation, all while discarding weights not in the [0, 1] range and optionally enforcing the sum to an arbitrary value:

import itertools
from typing import Optional, Tuple


def best_linear_approx(
    target: np.ndarray,
    components: np.ndarray,
) -> Tuple[np.ndarray, float]:
    coeffs, costs, _, _ = np.linalg.lstsq(components, target, rcond=None)
    return coeffs, np.sum(np.abs(costs))


def decompose(
    color,
    colors,
    max_nums: int = 3,
    min_weights: float = 0.0,
    max_weights: float = 1.0,
    acc_to: Optional[float] = None,
    use_avg = False,
    force_acc: bool = True,
) -> Tuple[Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], float, Optional[float]]:
    """Decompose `color` into a linear combination of `n` colors.

    The decomposition is subject to some constraints.

    Args:
        color: The color to decompose.
        colors: The base colors to use for the decomposition.
        max_nums: The maximum number of base colors to use.
        min_weights: The minimum value for the weights.
        max_weights: The maximum value for the weights.
        acc_to: The value the weights should accumulate (sum or average) to.
        use_avg: Use average instead of sum for composition / decomposition.
        force_acc: If True, the weights are normalized to `acc_to`, if set.

    Return:
        best_weights, approx_color, best_indices, best_selected, best_cost, acc_weights


    """
    color = np.array(color)
    colors = np.array(colors).T
    num_channels, num_colors = colors.shape
    if acc_to is not None:
        colors = np.concatenate(
            [colors, np.ones(num_colors, dtype=colors.dtype)[None, ...]], axis=0
        )
        color = np.concatenate([color, np.full(1, acc_to, dtype=colors.dtype)])
    for n in range(1, max_nums   1):
        best_indices = None
        best_weights = None
        best_cost = np.inf
        for indices in itertools.combinations(range(num_colors), n):
            if use_avg:
                color *= n
            weights, curr_cost = best_linear_approx(color, colors[:, indices])
            if force_acc and acc_to is not None:
                weights *= acc_to / np.sum(weights)
            if use_avg:
                weights *= n
            if (
                curr_cost < best_cost
                and (min_weights is None or np.all(weights >= min_weights))
                and (max_weights is None or np.all(weights <= max_weights))
            ):
                best_indices = indices
                best_weights = weights
                best_cost = curr_cost
            if use_avg:
                color /= n
    if best_indices is not None:
        approx_color = (colors[:, best_indices] @ best_weights)[:num_channels]
        acc_weights = np.sum(best_weights)
        if use_avg:
            n = len(best_indices)
            approx_color /= n
            acc_weights /= n
        best_selected = colors[:num_channels, best_indices].T
        
    else:
        approx_color = None
        best_selected = None
        acc_weights = None
    return best_weights, approx_color, best_indices, best_selected, best_cost, acc_weights

This can be used as follows:

from rich import print as rprint


colors = [
    [1.0, 0.0, 0.0],
    [0.0, 1.0, 0.0],
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 0.0],
    [0.94, 0.87, 0.8],
    [1.0, 1.0, 1.0],
    [0.8, 0.5, 0.2],
    [0.33, 0.41, 0.47],
    [0.76, 0.6, 0.42],
    [0.0, 0.75, 1.0],
    [0.77, 0.12, 0.23],
    [0.57, 0.63, 0.81],
    [0.67, 0.88, 0.69],
    [0.97, 0.91, 0.81],
    [0.21, 0.27, 0.31],
    [1.0, 0.99, 0.82],
    [0.0, 1.0, 1.0],
    [0.0, 0.0, 0.55],
    [1.0, 0.01, 0.24],
    [0.5, 0.5, 0.5],
    [0.39, 0.33, 0.32],
]


color = [0.52, 0.5, 0.5]

rprint(decompose(color, colors))
# (
#     array([0.52, 0.5 , 0.5 ]),
#     array([0.52, 0.5 , 0.5 ]),
#     (0, 1, 2),
#     array([[1., 0., 0.],
#        [0., 1., 0.],
#        [0., 0., 1.]]),
#     0.0,
#     1.52
# )
rprint(decompose(color, colors, acc_to=1))
# (
#     array([0.02, 0.48, 0.5 ]),
#     array([0.52, 0.5 , 0.5 ]),
#     (0, 3, 5),
#     array([[1., 0., 0.],
#        [0., 0., 0.],
#        [1., 1., 1.]]),
#     0.0,
#     1.0
# )
rprint(decompose(color, colors, acc_to=1, use_avg=True, max_weights=None))
# (
#     array([0.06, 1.44, 1.5 ]),
#     array([0.52, 0.5 , 0.5 ]),
#     (0, 3, 5),
#     array([[1., 0., 0.],
#        [0., 0., 0.],
#        [1., 1., 1.]]),
#     0.0,
#     1.0
# )
  • Related