如何创建从 2d 颜色图绘制颜色的 2d 直方图?

发布于 2025-01-12 06:30:37 字数 4763 浏览 0 评论 0原文

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

根据 matplotlib 的颜色图文档,亮度它们的默认颜色图的值不是恒定的。但是,我想从 HSL 颜色空间 创建一个具有恒定亮度的颜色图。我怎样才能做到这一点?

我一般认为,创建自己的颜色图并不难 ,但我不知道如何在满足亮度标准的同时做到这一点。也许这可以通过对颜色图文档中的代码进行逆向工程来完成?

解决方案

我认为我找到了一种方法来做到这一点,基于这篇文章。首先,事实证明,在 HSL 色彩空间中工作对于我的总体目标来说并不是最好的主意,因此我改用了 HSV。这样,我可以从 matplotlib 加载首选颜色图,从中创建一组 RGB 颜色,将它们转换为 HSV,设置它们的颜色值常量,将它们转换回 RGB,最后再次从它们创建颜色图(然后我可以用于二维直方图,例如)。

背景

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

例如,在下面的 MWE 中(从 此处 略有更改),使用具有恒定颜色值的颜色图,在每个 bin 中我可以使用饱和度来表示计数的数量(例如,颜色越浅,数量越少),并使用色调来表示平均z值。这将使我能够将下面的两个图合并为一个。 (还有关于添加 alpha 值的本教程到二维直方图,但我认为这在这种情况下不起作用。)

目前,您仍然需要两个图才能获得完整的图片,因为例如,如果没有直方图,您将无法判断某个特定值的重要性bin 中的 z 值可能是这样,因为使用相同的颜色与贡献它的数据点数量无关(因此根据颜色判断,只有一个数据点的 bin 可能看起来与具有相同颜色但包含更多数据点的容器;因此存在偏向于异常值的情况)。

输入图片这里的描述

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()

在此处输入图像描述

Old Question: How to create an HSL colormap in matplotlib with constant lightness?

According to 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 that has a constant lightness. How can I do that?

I get that generally, it's not that hard to create your own colormaps, but I don't know how to do this while satisfying the lightness criterion. Maybe this can be done by reverse-engineering the code from the colormap documentation?

Solution

I think I found a way to do that, based on this post. First of all, working in the HSL color space turned out to be not the best idea for my overal goal, so I switched to HSV instead. With that, I can load the preferred colormap from matplotlib, create a set of RGB colors from it, transform them into HSV, set their color value constant, transform them back into RGB and finally create a colormap from them again (which I can then use for a 2d histogram e.g.).

Background

I need a colormap in HSV with a constant color value because then I can uniquely map colors to the RGB space from the pallet that is spanned by hue and saturation. This in turn would allow me to create a 2d histogram where I could color-code both the counts (via the saturation) and a third variable (via the hue).

In the MWE below for example (slightly changed from here), 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 on adding alpha values to a 2d histogram, but this wouldn't work in this case I think.)

Currently, you still need both plots to get the full picture, because without the histogram for example, you wouldn't be able to tell how significant a certain z value in a bin might be, as the same color is used independently of how many data points contributed to it (so judging by the color, a bin with only one data point might look just as significant as a bin with the same color but that contains many more data points; thus there is a bias in favor of outliers).

enter image description here

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()

Remaining part of the issue

Now that I managed to find a way to create colormaps with constant color value, what remains is figuring out how to have the 2d histogram drawing from a 2d colormap. Since 2d histograms create an instance of a QuadMesh, and apparently you can set its facecolors, maybe that is a way to go about it, but I haven't figured out how. Below is my implementation of creating the 2d colormap at least:

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()

enter image description here

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

大海や 2025-01-19 06:30:37

我想到的是在您已经定义的 2D 色彩空间中进行插值。在使用 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]])

这就是你得到的
输入图片这里的描述

对于对数标度,您将对数应用于限制,但使用等空间网格。插值时使用坐标的对数。

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]])

输入图片此处描述

What comes to my mind is to interpolate in the 2D colorspace you already defined. Running the following code after your last example with n=100000 for smoother images.

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]])

This is what you get
enter image description here

For logarithmic scale you apply the logarithm to the limits but use equally space grid. When interpolating you use the logarithm of the coordinate.

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]])

enter image description here

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文