您正在设置的system of linear equations 是过度确定的,这意味着通常没有解决方案。
对比例(或更准确地说是权重)的额外约束——总和为 1,在 [0, 1] 范围内——使事情变得更糟,因为即使存在解决方案,它也可能因为这些额外的约束而被丢弃.
当前形式的问题在数学上是不可解的。
无论您是否要包含固定总和约束,为线性组合寻找最佳权重的数学非常相似,尽管并不总是可以获得精确的解决方案,但有可能获得近似解决方案。
计算 的一种方法是通过线性编程,它基本上可以让您到达@greenerpastures's answer,但需要您使用线性编程。
使用蛮力和简单的最小二乘
在这里,我提出了一种更基本的方法,其中只涉及简单的线性代数,但忽略了权重在[0, 1] 范围内的要求(以后可能会介绍)。
写出的方程目标颜色b 作为颜色的线性组合可以写成矩阵形式:
A x = b
A 由您要使用的颜色组成,b 是目标颜色和x 是权重。
/ r0 r1 r2 / r_
| g0 g1 g2 | (x0, x1, x2) = | g_ |
b0 b1 b2 / b_ /
现在,如果det(A) != 0,这个方程组承认一个单一的解决方案。
由于在选定的颜色中有一个正交基,您实际上可以使用它们来构造一个A 和det(A) != 0,因此总是可以找到一个x。
如果b 的元素在[0, 1] 范围内,那么x 的元素也是如此,因为本质上是b = x。
一般来说,Ax = b线性方程组与np.linalg.solve()的解,只要det(A) != 0,当A由其他颜色组成时,可以用来寻找x。
如果您想包含的颜色数量少于或少于通道数,则近似可以使用实现了least squares approximation 的np.linalg.lstsq() 获得最小化平方和的解决方案:找到分配给n 向量(components)的最佳权重,使得它们的线性组合(加权和)尽可能接近(最小化平方和)到target 向量。
一旦你准备好寻找近似解,要求权重之和成为线性方程组中的附加参数。
这可以通过简单地增加A 和b 来包含,并为A 设置一个额外的维度为1,为b 设置一个q,这样A x = b 变为:
/ r0 r1 r2 / r3
| g0 g1 g2 | (p0, p1, p2) = | g3 |
| b0 b1 b2 | | b3 |
1 1 1 / q /
现在包含了新方程p0 + p1 + p2 = q。
虽然所有这些都可以使用任意颜色,但通过接近度选择的颜色不一定是很好地近似任意颜色的候选者。
例如,如果目标颜色是(1, 0, 1),而最接近的 3 个颜色恰好彼此成比例,比如(0.9, 0, 0)、(0.8, 0, 0)、(0.7, 0, 0),则最好使用更远的(0, 0, 0.5)但可以比(0.7, 0, 0) 做出更好的近似。
鉴于可能的组合数量相当少,可以尝试所有颜色,按固定递增大小的组。
这种方法称为蛮力,因为我们尝试了所有方法。
最小二乘法用于求权重。
然后我们可以添加额外的逻辑来强制执行我们想要的约束。
为了强制权重总和为 1,可以显式地对它们进行归一化。
为了将它们限制在特定范围内,我们可以丢弃不符合要求的权重(可能有一些容差atol 以减轻浮点比较的数值问题)。
代码如下:
import itertools
import dataclasses
from typing import Optional, Tuple, Callable, Sequence
import numpy as np
def best_linear_approx(target: np.ndarray, components: np.ndarray) -> np.ndarray:
coeffs, _, _, _ = np.linalg.lstsq(components, target, rcond=-1)
return coeffs
@dataclasses.dataclass
class ColorDecomposition:
color: np.ndarray
weights: np.ndarray
components: np.ndarray
indices: np.ndarray
cost: float
sum_weights: float
def decompose_bf_lsq(
color: Sequence,
colors: Sequence,
max_nums: int = 3,
min_nums: int = 1,
min_weights: float = 0.0,
max_weights: float = 1.0,
atol: float = 1e-6,
norm_in_cost: bool = False,
force_norm: bool = False,
) -> Optional[ColorDecomposition]:
"""Decompose `color` into a linear combination of a number of `colors`.
This perfoms a brute-force search.
Some constraints can be introduced into the decomposition:
- The weights within a certain range ([`min_weights`, `max_weights`])
- The weights to accumulate (sum or average) to a certain value.
The colors are chosen to have minimum sum of squared differences (least squares).
Additional costs may be introduced in the brute-force search, to favor
particular solutions when the least squares are the same.
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.
atol: The tolerance on the weights.
norm_in_cost: Include the norm in the cost for the least squares.
force_norm: If True, the weights are normalized to `acc_to`, if set.
weight_costs: The additional weight costs to prefer specific solutions.
Returns:
The resulting color decomposition.
"""
color = np.array(color)
colors = np.array(colors)
num_colors, num_channels = colors.shape
# augment color/colors
if norm_in_cost:
colors = np.concatenate(
[colors, np.ones(num_colors, dtype=colors.dtype)[:, None]],
axis=1,
)
color = np.concatenate([color, np.ones(1, dtype=colors.dtype)])
# brute-force search
best_indices = None
best_weights = np.zeros(1)
best_cost = np.inf
for n in range(min_nums, max_nums + 1):
for indices in itertools.combinations(range(num_colors), n):
if np.allclose(color, np.zeros_like(color)):
# handles the 0 case
weights = np.ones(n)
else:
# find best linear approx
weights = best_linear_approx(color, colors[indices, :].T)
# weights normalization
if force_norm and np.all(weights > 0.0):
norm = np.sum(weights)
weights /= norm
# add some tolerance
if atol > 0:
mask = np.abs(weights) > atol
weights = weights[mask]
indices = np.array(indices)[mask].tolist()
if atol > 0 and max_weights is not None:
mask = (weights > max_weights - atol) & (weights < max_weights + atol)
weights[mask] = max_weights
if atol > 0 and min_weights is not None:
mask = (weights < min_weights + atol) & (weights > min_weights - atol)
weights[mask] = min_weights
# compute the distance between the current approximation and the target
err = color - (colors[indices, :].T @ weights)
curr_cost = np.sum(err * err)
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 best_indices is not None:
return ColorDecomposition(
color=(colors[best_indices, :].T @ best_weights)[:num_channels],
weights=best_weights,
components=[c for c in colors[best_indices, :num_channels]],
indices=best_indices,
cost=best_cost,
sum_weights=np.sum(best_weights),
)
else:
return None
这可以按如下方式使用:
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],
]
some_colors = [[0.9, 0.6, 0.5], [0.52, 0.5, 0.5], [0.5, 0.5, 0.5], [0, 0, 0], [1, 1, 1]]
# some_colors = [[0., 0., 0.]]
for color in some_colors:
print(color)
print(decompose_bf_lsq(color, colors, max_nums=1))
print(decompose_bf_lsq(color, colors, max_nums=2))
print(decompose_bf_lsq(color, colors))
print(decompose_bf_lsq(color, colors, min_weights=0.0, max_weights=1.0))
print(decompose_bf_lsq(color, colors, norm_in_cost=True))
print(decompose_bf_lsq(color, colors, force_norm=True))
print(decompose_bf_lsq(color, colors, norm_in_cost=True, force_norm=True))
# [0.9, 0.6, 0.5]
# ColorDecomposition(color=array([0.72956991, 0.68444188, 0.60922849]), weights=array([0.75213393]), components=[array([0.97, 0.91, 0.81])], indices=[13], cost=0.048107706898684606, sum_weights=0.7521339326213355)
# ColorDecomposition(color=array([0.9 , 0.60148865, 0.49820272]), weights=array([0.2924357, 0.6075643]), components=[array([1., 0., 0.]), array([1. , 0.99, 0.82])], indices=[0, 15], cost=5.446293494705139e-06, sum_weights=0.8999999999999999)
# ColorDecomposition(color=array([0.9, 0.6, 0.5]), weights=array([0.17826087, 0.91304348, 0.43478261]), components=[array([0., 0., 1.]), array([0.8, 0.5, 0.2]), array([0.39, 0.33, 0.32])], indices=[2, 6, 20], cost=0.0, sum_weights=1.526086956521739)
# ColorDecomposition(color=array([0.9, 0.6, 0.5]), weights=array([0.17826087, 0.91304348, 0.43478261]), components=[array([0., 0., 1.]), array([0.8, 0.5, 0.2]), array([0.39, 0.33, 0.32])], indices=[2, 6, 20], cost=0.0, sum_weights=1.526086956521739)
# ColorDecomposition(color=array([0.9, 0.6, 0.5]), weights=array([0.4, 0.1, 0.5]), components=[array([1., 0., 0.]), array([0., 1., 0.]), array([1., 1., 1.])], indices=[0, 1, 5], cost=2.6377536518327582e-30, sum_weights=0.9999999999999989)
# ColorDecomposition(color=array([0.9, 0.6, 0.5]), weights=array([0.4, 0.1, 0.5]), components=[array([1., 0., 0.]), array([0., 1., 0.]), array([1., 1., 1.])], indices=[0, 1, 5], cost=3.697785493223493e-32, sum_weights=0.9999999999999999)
# ColorDecomposition(color=array([0.9, 0.6, 0.5]), weights=array([0.4, 0.1, 0.5]), components=[array([1., 0., 0.]), array([0., 1., 0.]), array([1., 1., 1.])], indices=[0, 1, 5], cost=1.355854680848614e-31, sum_weights=1.0)
# [0.52, 0.5, 0.5]
# ColorDecomposition(color=array([0.50666667, 0.50666667, 0.50666667]), weights=array([0.50666667]), components=[array([1., 1., 1.])], indices=[5], cost=0.0002666666666666671, sum_weights=0.5066666666666667)
# ColorDecomposition(color=array([0.52, 0.5 , 0.5 ]), weights=array([0.52, 0.5 ]), components=[array([1., 0., 0.]), array([0., 1., 1.])], indices=[0, 16], cost=2.465190328815662e-32, sum_weights=1.02)
# ColorDecomposition(color=array([0.52, 0.5 , 0.5 ]), weights=array([0.2 , 0.2 , 0.508]), components=[array([0.76, 0.6 , 0.42]), array([0.57, 0.63, 0.81]), array([0.5, 0.5, 0.5])], indices=[8, 11, 19], cost=0.0, sum_weights=0.9079999999999999)
# ColorDecomposition(color=array([0.52, 0.5 , 0.5 ]), weights=array([0.2 , 0.2 , 0.508]), components=[array([0.76, 0.6 , 0.42]), array([0.57, 0.63, 0.81]), array([0.5, 0.5, 0.5])], indices=[8, 11, 19], cost=0.0, sum_weights=0.9079999999999999)
# ColorDecomposition(color=array([0.52, 0.5 , 0.5 ]), weights=array([0.02, 0.48, 0.5 ]), components=[array([1., 0., 0.]), array([0., 0., 0.]), array([1., 1., 1.])], indices=[0, 3, 5], cost=2.0954117794933126e-31, sum_weights=0.9999999999999996)
# ColorDecomposition(color=array([0.52, 0.5 , 0.5 ]), weights=array([0.02, 1. ]), components=[array([1., 0., 0.]), array([0.5, 0.5, 0.5])], indices=[0, 19], cost=0.0, sum_weights=1.02)
# ColorDecomposition(color=array([0.52, 0.5 , 0.5 ]), weights=array([0.02, 0.02, 0.96]), components=[array([1., 0., 0.]), array([1., 1., 1.]), array([0.5, 0.5, 0.5])], indices=[0, 5, 19], cost=9.860761315262648e-32, sum_weights=1.0)
# [0.5, 0.5, 0.5]
# ColorDecomposition(color=array([0.5, 0.5, 0.5]), weights=array([1.]), components=[array([0.5, 0.5, 0.5])], indices=[19], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0.5, 0.5, 0.5]), weights=array([1.]), components=[array([0.5, 0.5, 0.5])], indices=[19], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0.5, 0.5, 0.5]), weights=array([1.]), components=[array([0.5, 0.5, 0.5])], indices=[19], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0.5, 0.5, 0.5]), weights=array([1.]), components=[array([0.5, 0.5, 0.5])], indices=[19], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0.5, 0.5, 0.5]), weights=array([1.]), components=[array([0.5, 0.5, 0.5])], indices=[19], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0.5, 0.5, 0.5]), weights=array([1.]), components=[array([0.5, 0.5, 0.5])], indices=[19], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0.5, 0.5, 0.5]), weights=array([1.]), components=[array([0.5, 0.5, 0.5])], indices=[19], cost=0.0, sum_weights=1.0)
# [0, 0, 0]
# ColorDecomposition(color=array([0., 0., 0.]), weights=array([1.]), components=[array([0., 0., 0.])], indices=[3], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0., 0., 0.]), weights=array([1.]), components=[array([0., 0., 0.])], indices=[3], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0., 0., 0.]), weights=array([1.]), components=[array([0., 0., 0.])], indices=[3], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0., 0., 0.]), weights=array([1.]), components=[array([0., 0., 0.])], indices=[3], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0., 0., 0.]), weights=array([1.]), components=[array([0., 0., 0.])], indices=[3], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0., 0., 0.]), weights=array([1.]), components=[array([0., 0., 0.])], indices=[3], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0., 0., 0.]), weights=array([1.]), components=[array([0., 0., 0.])], indices=[3], cost=0.0, sum_weights=1.0)
# [1, 1, 1]
# ColorDecomposition(color=array([1., 1., 1.]), weights=array([1.]), components=[array([1., 1., 1.])], indices=[5], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([1., 1., 1.]), weights=array([1.]), components=[array([1., 1., 1.])], indices=[5], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([1., 1., 1.]), weights=array([0.1610306 , 0.96618357, 0.28692724]), components=[array([0.21, 0.27, 0.31]), array([1. , 0.99, 0.82]), array([0. , 0. , 0.55])], indices=[14, 15, 17], cost=0.0, sum_weights=1.4141414141414144)
# ColorDecomposition(color=array([1., 1., 1.]), weights=array([0.1610306 , 0.96618357, 0.28692724]), components=[array([0.21, 0.27, 0.31]), array([1. , 0.99, 0.82]), array([0. , 0. , 0.55])], indices=[14, 15, 17], cost=0.0, sum_weights=1.4141414141414144)
# ColorDecomposition(color=array([1., 1., 1.]), weights=array([1.]), components=[array([1., 1., 1.])], indices=[5], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([1., 1., 1.]), weights=array([1.]), components=[array([1., 1., 1.])], indices=[5], cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([1., 1., 1.]), weights=array([1.]), components=[array([1., 1., 1.])], indices=[5], cost=0.0, sum_weights=1.0)
使用蛮力和有界最小化
这与上面基本相同,除了现在我们使用比简单的无界最小二乘法更复杂的优化方法。
这为我们提供了已经有界的权重,因此不需要额外的代码来处理这种情况,最重要的是,仅根据成本就放弃了最佳解决方案。
该方法将显示为:
import scipy.optimize
def _err(x, A, b):
return b - A @ x
def cost(x, A, b):
err = _err(x, A, b)
return np.sum(err * err)
def decompose_bf_min(
color: Sequence,
colors: Sequence,
max_nums: int = 3,
min_nums: int = 1,
min_weights: float = 0.0,
max_weights: float = 1.0,
normalize: bool = False,
) -> Optional[ColorDecomposition]:
color = np.array(color)
colors = np.array(colors)
num_colors, num_channels = colors.shape
# augment color/colors to include norm in cost
if normalize:
colors = np.concatenate(
[colors, np.ones(num_colors, dtype=colors.dtype)[:, None]],
axis=1,
)
color = np.concatenate([color, np.ones(1, dtype=colors.dtype)])
# brute-force search
best_indices = None
best_weights = np.zeros(1)
best_cost = np.inf
for n in range(min_nums, max_nums + 1):
for indices in itertools.combinations(range(num_colors), n):
weights = np.full(n, 1 / n)
if not np.allclose(color, 0):
res = scipy.optimize.minimize(
cost,
weights,
(colors[indices, :].T, color),
bounds=[(min_weights, max_weights) for _ in range(n)]
)
weights = res.x
curr_cost = cost(weights, colors[indices, :].T, color)
if curr_cost <= best_cost:
best_indices = indices
best_weights = weights
best_cost = curr_cost
if best_indices is not None:
return ColorDecomposition(
color=(colors[best_indices, :].T @ best_weights)[:num_channels],
weights=best_weights,
components=[c for c in colors[best_indices, :num_channels]],
indices=best_indices,
cost=best_cost,
sum_weights=np.sum(best_weights),
)
else:
return None
其工作原理如下:
some_colors = [[0.9, 0.6, 0.5], [0.52, 0.5, 0.5], [0.5, 0.5, 0.5], [0, 0, 0], [1, 1, 1]]
# some_colors = [[0., 0., 0.]]
for color in some_colors:
print(color)
print(decompose_bf_min(color, colors))
print(decompose_bf_min(color, colors, normalize=True))
# [0.9, 0.6, 0.5]
# ColorDecomposition(color=array([0.9, 0.6, 0.5]), weights=array([0.42982455, 0.2631579 , 0.70701754]), components=[array([0.8, 0.5, 0.2]), array([0.77, 0.12, 0.23]), array([0.5, 0.5, 0.5])], indices=(6, 10, 19), cost=2.3673037349051385e-17, sum_weights=1.399999995602849)
# ColorDecomposition(color=array([0.89999998, 0.60000001, 0.49999999]), weights=array([0.4 , 0.10000003, 0.49999999]), components=[array([1., 0., 0.]), array([0., 1., 0.]), array([1., 1., 1.])], indices=(0, 1, 5), cost=6.957464274781682e-16, sum_weights=1.0000000074212045)
# [0.52, 0.5, 0.5]
# ColorDecomposition(color=array([0.52, 0.5 , 0.5 ]), weights=array([0.02, 0. , 1. ]), components=[array([1., 0., 0.]), array([1. , 0.99, 0.82]), array([0.5, 0.5, 0.5])], indices=(0, 15, 19), cost=2.1441410828292465e-17, sum_weights=1.019999995369513)
# ColorDecomposition(color=array([0.52000021, 0.50000018, 0.50000018]), weights=array([0.02000003, 0.02000077, 0.95999883]), components=[array([1., 0., 0.]), array([1., 1., 1.]), array([0.5, 0.5, 0.5])], indices=(0, 5, 19), cost=2.517455337509621e-13, sum_weights=0.9999996259509482)
# [0.5, 0.5, 0.5]
# ColorDecomposition(color=array([0.5, 0.5, 0.5]), weights=array([0., 0., 1.]), components=[array([0., 1., 1.]), array([1. , 0.01, 0.24]), array([0.5, 0.5, 0.5])], indices=(16, 18, 19), cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0.5, 0.5, 0.5]), weights=array([0., 1., 0.]), components=[array([1. , 0.01, 0.24]), array([0.5, 0.5, 0.5]), array([0.39, 0.33, 0.32])], indices=(18, 19, 20), cost=0.0, sum_weights=1.0)
# [0, 0, 0]
# ColorDecomposition(color=array([0., 0., 0.]), weights=array([1.]), components=[array([0., 0., 0.])], indices=(3,), cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([0., 0., 0.]), weights=array([1., 0., 0.]), components=[array([0., 0., 0.]), array([0. , 0. , 0.55]), array([1. , 0.01, 0.24])], indices=(3, 17, 18), cost=0.0, sum_weights=1.0)
# [1, 1, 1]
# ColorDecomposition(color=array([1., 1., 1.]), weights=array([1., 0., 0.]), components=[array([1., 1., 1.]), array([0., 1., 1.]), array([1. , 0.01, 0.24])], indices=(5, 16, 18), cost=0.0, sum_weights=1.0)
# ColorDecomposition(color=array([1., 1., 1.]), weights=array([1., 0., 0.]), components=[array([1., 1., 1.]), array([1. , 0.01, 0.24]), array([0.5, 0.5, 0.5])], indices=(5, 18, 19), cost=0.0, sum_weights=1.0)