如何创建从二维颜色图中绘制颜色的二维直方图?

How to create a 2d histogram that draws its colors from a 2d colormap?

老问题:如何在 matplotlib 中创建具有恒定亮度的 HSL 颜色图?

根据 matplotlib's colormap documentation, the lightness values of their default colormaps are not constant. However, I would like to create a colormap from the HSL color space 具有恒定的亮度。我该怎么做?

我一般都知道,it's not that hard to create your own colormaps,但我不知道如何在满足亮度标准的同时做到这一点。也许这可以通过对颜色图文档中的代码进行逆向工程来完成?

解决方案

我想我找到了一种基于 this post 的方法。首先,事实证明,使用 HSL 颜色 space 并不是我总体目标的最佳选择,因此我转而使用 HSV。有了它,我可以从 matplotlib 加载首选颜色图,从中创建一组 RGB 颜色,将它们转换为 HSV,设置它们的颜色值常量,将它们转换回 RGB,最后再次从它们创建颜色图(然后我可以用于二维直方图,例如)。

背景

我需要一个具有恒定颜色值的 HSV 颜色图,因为这样我就可以将颜色从色相和饱和度跨越的调色板唯一地映射到 RGB space。这反过来又允许我创建一个二维直方图,我可以在其中对计数(通过饱和度)和第三个变量(通过色调)进行颜色编码。

例如在下面的 MWE 中(在将 alpha 值添加到二维直方图时与 ), with a colormap with constant color value, in each bin I could use the saturation to indicate the number of counts (e.g. the lighter the color, the lower the number), and use the hue to indicate the the average z value. This would allow me to essentially combine the two plots below into one. (There is also this tutorial 略有不同,但我认为这在这种情况下不起作用。)

目前,您仍然需要这两个图才能获得完整的图片,因为如果没有直方图,您将无法判断 bin 中某个 z 值的重要性,因为相同颜色的使用与多少数据点无关(因此根据颜色判断,只有一个数据点的箱子看起来可能与具有相同颜色但包含更多数据点的箱子一样重要;因此有是偏向于离群值的偏差)。

import matplotlib.pyplot as plt
import numpy as np


# make data: correlated + noise
n = 1000
x, y = np.random.uniform(-2, 2, (2, n))
z = np.sqrt(x**2 + y**2) + np.random.uniform(0, 1, n)

bins = 20
fig, axs = plt.subplots(1, 2, figsize=(7, 3), constrained_layout=True)
_, _, _, img = axs[0].hist2d(x, y, bins=bins)
fig.colorbar(img, ax=axs[0])
axs[0].set(xlabel='x', ylabel='y', title='histogram')

sums, xbins, ybins = np.histogram2d(x, y, bins=bins, weights=z)
counts, _, _ = np.histogram2d(x, y, bins=bins)
with np.errstate(divide='ignore', invalid='ignore'):
    # suppress possible divide-by-zero warnings
    img = axs[1].pcolormesh(xbins, ybins, sums / counts, cmap='inferno')
fig.colorbar(img, ax=axs[1], label='z')
axs[1].set(xlabel='x', ylabel='y', title='weighed by z')
fig.show()

问题的剩余部分

既然我设法找到了一种方法来创建具有恒定颜色值的颜色图,剩下的就是弄清楚如何从 2d 颜色图中绘制 2d 直方图。由于 2d 直方图创建了一个 QuadMesh 的实例,显然你可以设置它的面部颜色,也许这是一种解决方法,但我还没有弄清楚如何。下面是我至少创建 2d 颜色图的实现:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
from matplotlib.colors import hsv_to_rgb, rgb_to_hsv, ListedColormap

# make data: correlated + noise
np.random.seed(100)
n = 1000
x, y = np.random.uniform(-2, 2, (2, n))
z = np.sqrt(x**2 + y**2) + np.random.uniform(0, 1, n)

bins = 20
fig, axs = plt.subplots(1, 3, figsize=(8, 3), constrained_layout=True)
_, _, _, img = axs[0].hist2d(x, y, bins=bins)
fig.colorbar(img, ax=axs[0], label='N')
axs[0].set(xlabel='x', ylabel='y', title='histogram')

# creating the colormap
inferno = cm.get_cmap('inferno')
hsv_inferno = rgb_to_hsv(inferno(np.linspace(0, 1, 300))[:, :3])
hsv_inferno[:, 2] = 1
rgb_inferno = hsv_to_rgb(hsv_inferno)

# plotting the data
sums, xbins, ybins = np.histogram2d(x, y, bins=bins, weights=z)
counts, _, _ = np.histogram2d(x, y, bins=bins)
with np.errstate(divide='ignore', invalid='ignore'):
    # suppress possible divide-by-zero warnings
    img = axs[1].pcolormesh(
        xbins, ybins, sums / counts, cmap=ListedColormap(rgb_inferno)
    )
axs[1].set(xlabel='x', ylabel='y', title='weighed by z')

# adding the custom colorbar
S, H = np.mgrid[0:1:100j, 0:1:300j]
V = np.ones_like(S)
HSV = np.dstack((H, S, V))
HSV[:, :, 0] = hsv_inferno[:, 0]
# HSV[:, :, 2] = hsv_inferno[:, 2]
RGB = hsv_to_rgb(HSV)
z_min, z_max = np.min(img.get_array()), np.max(img.get_array())
c_min, c_max = np.min(counts), np.max(counts)
axs[2].imshow(
    np.rot90(RGB), origin='lower', extent=[c_min, c_max, z_min, z_max],
    aspect=14
)
axs[2].set_xlabel("N")
axs[2].set_ylabel("z")
axs[2].yaxis.set_label_position("right")
axs[2].yaxis.tick_right()

# readjusting the axes a bit
fig.show()  # necessary to get the proper positions
pos = axs[1].get_position()
pos.x0 += 0.065
pos.x1 += 0.065
axs[1].set_position(pos)
fig.show()

我想到的是在您已经定义的 2D 颜色space 中进行插值。 运行 在上一个示例之后的以下代码 n=100000 以获得更平滑的图像。

from scipy import interpolate 

z = np.divide(sums, counts, where=counts != 0);
points = np.mgrid[
    0:np.max(counts):1j*RGB.shape[0], # use counts for the first axis
    0:np.max(z):1j*RGB.shape[1], # use sum in for the second axis
]
# arrange points in a N x 2 array
points = np.stack(points, axis=2).reshape(-1, 2)
# arrange the colors in a N x 3 array
values =  RGB.reshape(-1, 3) # use your 2D colormap as values

# Creates an interpolator from (..., 2) to (..., 3)
cmap2d = interpolate.LinearNDInterpolator(
    points, values
)
# stack counts and sums in an array of (n1, n2, 2)
cpoints = np.stack([counts, z], axis=2)
# gets an (n1, n2, 3) array
img = cmap2d(cpoints)
# plot the img as a RGB image
plt.imshow(img, extent=[xbins[0], xbins[-1], ybins[0], ybins[-1]])

这就是你得到的

对于对数刻度,您将对数应用于限制,但同样使用 space 网格。插值时使用坐标的对数。

from scipy import interpolate 

z = np.divide(sums, counts, where=counts != 0);
points = np.mgrid[
    # apply log to the limits from 1/e to max(count)
    -1:np.log(np.max(counts)):1j*RGB.shape[0], # use counts for the first axis
    0:np.max(z):1j*RGB.shape[1], # use sum in for the second axis
]
# arrange points in a N x 2 array
points = np.stack(points, axis=2).reshape(-1, 2)
# arrange the colors in a N x 3 array
values =  RGB.reshape(-1, 3) # use your 2D colormap as values

# Creates an interpolator from (..., 2) to (..., 3)
cmap2d = interpolate.LinearNDInterpolator(
    points, values
)
# stack counts and sums in an array of (n1, n2, 2)
# apply log to the values
cpoints = np.stack([np.log(np.maximum(counts, 1)), z], axis=2)
# gets an (n1, n2, 3) array
img = cmap2d(cpoints)
# plot the img as a RGB image
plt.imshow(img, extent=[xbins[0], xbins[-1], ybins[0], ybins[-1]])