计算一系列数值的RGB值以创建热力图

32

我试图用Python创建一个热力图。为此,我需要为可能值的每个值分配一个RGB值。我考虑将颜色从蓝色(最小值)变成绿色,再到红色(最大值)。

下面的图片示例解释了我如何考虑颜色组成:我们有从1(纯蓝色)到3(纯红色)的范围,2位于中间,由绿色表示。

在范围(1-3)内的颜色组合RGB

我阅读了关于线性插值的内容,并编写了一个函数来处理在最小值和最大值之间的某个值的计算,并返回RGB元组。它使用ifelif条件(这并不能让我完全满意):

def convert_to_rgb(minimum, maximum, value):
    minimum, maximum = float(minimum), float(maximum)    
    halfmax = (minimum + maximum) / 2
    if minimum <= value <= halfmax:
        r = 0
        g = int( 255./(halfmax - minimum) * (value - minimum))
        b = int( 255. + -255./(halfmax - minimum)  * (value - minimum))
        return (r,g,b)    
    elif halfmax < value <= maximum:
        r = int( 255./(maximum - halfmax) * (value - halfmax))
        g = int( 255. + -255./(maximum - halfmax)  * (value - halfmax))
        b = 0
        return (r,g,b)

不过,我在想是否可以为每个颜色值编写一个函数,而不使用if条件语句。有人有想法吗?非常感谢!

6个回答

45
def rgb(minimum, maximum, value):
    minimum, maximum = float(minimum), float(maximum)
    ratio = 2 * (value-minimum) / (maximum - minimum)
    b = int(max(0, 255*(1 - ratio)))
    r = int(max(0, 255*(ratio - 1)))
    g = 255 - b - r
    return r, g, b

2
halfmax 应计算为 (minimum - maximum) / 2,而 value/halfmax 应为 (value - minimum)/halfmax,否则仅在 minimum 为1且 maximum 为3时才能正常工作。参见:http://codereview.stackexchange.com/a/64720/7641 - Guffa

22

这里有另一种方法可以实现,虽然不是绝对最短的,但更为通用,因为它没有为您特定的颜色集硬编码。这意味着它也可以用于在线性插值可变大小的任意颜色调色板上指定范围的值。

此外,请注意颜色可以在其他颜色空间中进行插值,得到的结果可能比其他更令人愉悦。这可以从我提交的两个不同答案所示,这些答案涉及一个名为范围值到伪彩色的相关问题。

import sys
EPSILON = sys.float_info.epsilon  # Smallest possible difference.

def convert_to_rgb(minval, maxval, val, colors):
    # `colors` is a series of RGB colors delineating a series of
    # adjacent linear color gradients between each pair.

    # Determine where the given value falls proportionality within
    # the range from minval->maxval and scale that fractional value
    # by the total number in the `colors` palette.
    i_f = float(val-minval) / float(maxval-minval) * (len(colors)-1)

    # Determine the lower index of the pair of color indices this
    # value corresponds and its fractional distance between the lower
    # and the upper colors.
    i, f = int(i_f // 1), i_f % 1  # Split into whole & fractional parts.

    # Does it fall exactly on one of the color points?
    if f < EPSILON:
        return colors[i]
    else: # Return a color linearly interpolated in the range between it and 
          # the following one.
        (r1, g1, b1), (r2, g2, b2) = colors[i], colors[i+1]
        return int(r1 + f*(r2-r1)), int(g1 + f*(g2-g1)), int(b1 + f*(b2-b1))

if __name__ == '__main__':
    minval, maxval = 1, 3
    steps = 10
    delta = float(maxval-minval) / steps
    colors = [(0, 0, 255), (0, 255, 0), (255, 0, 0)]  # [BLUE, GREEN, RED]
    print('  Val       R    G    B')
    for i in range(steps+1):
        val = minval + i*delta
        r, g, b = convert_to_rgb(minval, maxval, val, colors)
        print('{:.3f} -> ({:3d}, {:3d}, {:3d})'.format(val, r, g, b))

数字输出:

  Val       R    G    B
1.000 -> (  0,   0, 255)
1.200 -> (  0,  50, 204)
1.400 -> (  0, 101, 153)
1.600 -> (  0, 153, 101)
1.800 -> (  0, 204,  50)
2.000 -> (  0, 255,   0)
2.200 -> ( 51, 203,   0)
2.400 -> (102, 152,   0)
2.600 -> (153, 101,   0)
2.800 -> (203,  51,   0)
3.000 -> (255,   0,   0)

这是以水平渐变方式可视化的输出结果:

使用答案中的函数生成的水平渐变


1
我已经使用了这段代码,它的表现非常出色,即使在不同的颜色映射(红色、橙色、白色)下也是如此。通过在代码中添加注释,可以进一步完善这个解决方案,并帮助我们理解其中的理论和实践。例如,找到上述浮点数和整数之间的差异的目的是什么? - Wes Modes
一种观点是,“颜色”指定了一个通过2D颜色空间的线,线性输入被映射到该线上。 - Wes Modes
@Wes:这种观点的一个错误之处在于,大多数颜色空间都是三维的(例如RGB、YIQ和HLS),而不是二维的。 - martineau
我认为你的打印语句有误:它应该是print(' Val R G B')而不是print(' Val R B G')。然而,在你下面粘贴的数值输出中它是正确的。 - ThaNoob
@ThaNoob:发现得好。谢谢。 - martineau
显示剩余4条评论

2
"我们感知光强度的对数尺度 - 指数强度斜坡将被视为线性斜坡"。引自https://courses.cs.washington.edu/courses/cse455/09wi/Lects/lect11.pdfhttps://en.wikipedia.org/wiki/RGB_color_model中得知:"输入强度RGB值为(0.5,0.5,0.5)只输出完全亮度(1.0,1.0,1.0)的约22%,而不是50%"
这导致了在@martineau的示例中出现2.5处的棕色污点,而应该是黄色,并且在1.5处出现青色,以获得适当的色调渐变。
因此,您应该使用的公式可能不是您想要的。(很抱歉没有直接回答您的问题)
但是,将其转换为HSV或HLS颜色空间模型可能会很方便,然后使用H(用于色调),并将其用于输入,最后再将其转换回RGB进行显示。
colorsys.hsv_to_rgb(value, 1, 1)

https://docs.python.org/2/library/colorsys.html


这是最好的答案!从某种意义上来说,也是唯一正确的答案。 - bias

2

您可以经常使用一个包含两个值的数组来消除 if 语句。尽管 Python 没有三元条件运算符,但是可以使用以下方法:

r = [red_curve_1, red_curve_2][value>=halfmax]
g = [green_curve_1, green_curve_2][value>=halfmax]
b = [blue_curve_1, blue_curve_2][value>=halfmax]

请将*_curve_1*_curve_2表达式替换为中点左侧或右侧的常量、斜率或曲线。具体的替换请由您完成,例如:
  • red_curve_1blue_curve_2都是0
  • green_curve_1255*(value-minimum)/(halfmax-minimum)
  • 等等。

这就是我所说的“条件索引”。顺便说一句,Python确实有一个称为 Conditional Expression 的三元操作符。它允许像 r = red_curve_1 if value >= halfmax else red_curve_2 这样的语句-尽管我想使用它可能会更明显地表明该方法真正没有摆脱OP试图消除的 if 条件。 - martineau
感谢您提醒我关于条件表达式的问题。实际上,它比我提出的条件索引更易读。但正如您所说,OP显然想要摆脱“if”。(条件表达式方法还具有在返回结果之前不评估所有内容的优点。) - Darren Stone

0

对于那些不想携带所有代码的人,"terminedia"包提供了一个渐变类,可以处理任意数量和位置的一般渐变。

生成的ColorGradient实例可以使用0到1之间的索引,在给定点获取所需的颜色。

例如,对于给定的颜色[(4, 4, 4), (226, 75, 20), (4, 162, 221)],可以执行以下操作:

In [286]: from terminedia import ColorGradient

In [287]: g =  ColorGradient([(0, (4,4,4)), (0.5, (226, 75, 20)), (1, (4, 162, 221))])

In [288]: g[0.2]
Out[288]: <Color (92, 32, 10)>

In [289]: print([tuple(g[i/25]) for i in range(26)])
[(4, 4, 4), (21, 9, 5), (39, 15, 6), (57, 21, 7), (75, 26, 9), (92, 32, 10), (110, 38, 11), (128, 43, 12), (146, 49, 14), (163, 55, 15), (181, 60, 16), (199, 66, 18), (217, 72, 19), (217, 78, 28), (199, 85, 44), (181, 92, 60), (163, 99, 76), (146, 106, 92), (128, 113, 108), (110, 120, 124), (92, 127, 140), (75, 134, 156), (57, 141, 172), (39, 148, 188), (21, 155, 204), (4, 162, 221)]

目前发布的 terminedia 版本(0.4.3)可以做到这一点 - 开发代码(https://github.com/jsbueno/terminedia/)在创建渐变时将停止位置设为可选项,并且颜色会自动均匀分布。这意味着在 0.4.3 版本之后的版本中,可以使用以下代码创建相同的渐变:g = ColorGradient( [(4, 4, 4), (226, 75, 20), (4, 162, 221)])


0
在Blender中进行测试后,您需要将值限制在最小值和最大值之间,然后结果就正确了。
import numpy as np

def convert_to_rgb(minimum, maximum, value):
    value = np.clip(value, minimum, maximum)
    minimum, maximum = float(minimum), float(maximum)
    ratio = 2 * (value-minimum) / (maximum - minimum)
    b = int(max(0, 255*(1 - ratio)))
    r = int(max(0, 255*(ratio - 1)))
    g = 255 - b - r
    return (r/255.0,g/255.0,b/255.0)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接