CIE Luv色空間 (CIE 1976 Luv color space)の色相環(hue wheel)をOpenCVとMatplotlibで描画する。

ぶっちゃけると色覚に関する研究もしていて、JNNS2019でポスター発表はしたが頓挫したりあっちこっちに行ったりしている。今回はその付随したメモである。色空間について、自分は今までHSV色空間を使うなどかなり適当だったが、先行研究がほとんどCIELUV色空間を使っていたので調べることとした。単なるHSVでは色の数値的差が知覚的差と一致しないが、CIELuvでは両者ができるだけ近くなるようにしている。

描画にはcolourまたはOpenCVを使う。Pythonのpackageであるcolourはpipで入る:pip install colour-science

なお、変換をscratchでする場合にはhttp://www.easyrgb.com/en/math.phpが参考になる。

OpenCVを使う場合

CIE Luv色空間において値域はL [0, 100], u[-100, 100], v[-100, 100]である。Hue angle の配列を作り、cos, sinに入れて100をかけ、それぞれをu, vとする。そしてLを適当に定めることでLuvの配列ができる。

後の面倒な変換はcv2.cvtColor(hogehoge, cv2.COLOR_Luv2RGB)に任せる(cf. 変換できる色空間の一覧)。単なる変換ではRGBのどれかの値が負の値を取るが、それを0にclippingしている。逆にcvtColorを使うと正規化されずにclippingされるので、L=100のときはほぼ白色となる。

変換式は下記参照(Miscellaneous Image Transformations — OpenCV 2.4.13.7 documentationより引用)。

image

色相バー

import matplotlib.pyplot as plt
import numpy as np
import cv2
 
N_theta = 1000
luv = np.zeros((1, N_theta, 3)).astype(np.float32)
theta = np.linspace(0, 2*np.pi, N_theta)
luv[:, :, 0] = 80 # L
luv[:, :, 1] = np.cos(theta)*100 # u
luv[:, :, 2] = np.sin(theta)*100 # v
 
rgb = cv2.cvtColor(luv, cv2.COLOR_Luv2RGB)
plt.imshow(rgb, vmin=0, vmax=1, aspect=100)
plt.show()

色相環

N_theta = 1000
luv = np.zeros((1, N_theta, 3)).astype(np.float32)
theta = np.linspace(0, 2*np.pi, N_theta)
luv[:, :, 0] = 80 # L
luv[:, :, 1] = np.cos(theta)*100 # u
luv[:, :, 2] = np.sin(theta)*100 # v
 
rgb = cv2.cvtColor(luv, cv2.COLOR_Luv2RGB)
# hue wheel plot
ax = plt.subplot(111, polar=True)
#get coordinates:
theta = np.linspace(0, 2*np.pi, rgb.shape[1]+1)
r = np.linspace(0.5, 1, rgb.shape[0]+1)
Theta,R = np.meshgrid(theta, r)
 
# get color
color = rgb.reshape((rgb.shape[0]*rgb.shape[1], rgb.shape[2]))
m = plt.pcolormesh(theta,R, rgb[:,:,0], color=color, linewidth=0)
# This is necessary to let the `color` argument determine the color
m.set_array(None)
plt.show()

uv平面

uv平面をplotすると次のようになる。

W = 1000
H = 1000
luv = np.zeros((W, H, 3)).astype(np.float32)
u = np.linspace(-100, 100, W) # u
v = np.linspace(-100, 100, H) # v
U, V = np.meshgrid(u, v)
luv[:, :, 0] = 80
luv[:, :, 1] = U
luv[:, :, 2] = V
 
RGB = cv2.cvtColor(luv, cv2.COLOR_Luv2RGB)
plt.figure(figsize=(5,5))
plt.imshow(RGB, vmin=0, vmax=1)
plt.show()

Colourを使う場合

colourは色の変換関数が充実している。そこでu', v'平面上の円周上の色を、xyに変換、さらにXYZに変換し(このときY=1とする)、最後にRGBに変換したものをplotする。

色相バー

HSVと比べると青がかなり短いことが分かる。

import colour
 
N_theta = 500
theta = np.linspace(0, 2*np.pi, N_theta)
r = 0.2
u = np.cos(theta)*r + 0.2009
v = np.sin(theta)*r + 0.4610
uv = np.dstack((u, v))
 
# map -> xy -> XYZ -> sRGB
xy = colour.Luv_uv_to_xy(uv)
xyz = colour.xy_to_XYZ(xy)
rgb = colour.XYZ_to_sRGB(xyz)
rgb = colour.utilities.normalise_maximum(rgb, axis=-1)
 
plt.figure(figsize=(5,3))
plt.imshow(np.reshape(rgb, (1, N_theta, 3)), aspect=100)
plt.show()
N_theta = 500
theta = np.linspace(0, 2*np.pi, N_theta)
r = 0.2
u = np.cos(theta)*r + 0.2009
v = np.sin(theta)*r + 0.4610
uv = np.dstack((u, v))
 
# map -> xy -> XYZ -> sRGB
xy = colour.Luv_uv_to_xy(uv)
xyz = colour.xy_to_XYZ(xy)
rgb = colour.XYZ_to_sRGB(xyz)
rgb = colour.utilities.normalise_maximum(rgb, axis=-1)
 
# hue wheel plot
ax = plt.subplot(111, polar=True)
#get coordinates:
theta = np.linspace(0, 2*np.pi, rgb.shape[1]+1)
r = np.linspace(0.5, 1, rgb.shape[0]+1)
Theta,R = np.meshgrid(theta, r)
 
# get color
color = rgb.reshape((rgb.shape[0]*rgb.shape[1], rgb.shape[2]))
m = plt.pcolormesh(theta,R, rgb[:,:,0], color=color, linewidth=0)
# This is necessary to let the `color` argument determine the color
m.set_array(None)
plt.show()

CIE 1976 UCS (uniform chromaticity scale) diagram

描画方法を2つ示す。

Method 1

黒線は光のスペクトルに対応する点を意味する。

import matplotlib.pyplot as plt
import numpy as np
import colour
 
samples = 258
xlim = (0, 1)
ylim = (0, 1)
 
wvl = np.arange(420, 700, 5)
wvl_XYZ = colour.wavelength_to_XYZ(wvl)
wvl_uv = colour.Luv_to_uv(colour.XYZ_to_Luv(wvl_XYZ))
wvl_pts = wvl_uv * samples
 
u = np.linspace(xlim[0], xlim[1], samples)
v = np.linspace(ylim[0], ylim[1], samples)
uu, vv = np.meshgrid(u, v)
 
# stack u and v for vectorized computations
uuvv = np.stack((vv,uu), axis=2)
 
# map -> xy -> XYZ -> sRGB
xy = colour.Luv_uv_to_xy(uuvv)
xyz = colour.xy_to_XYZ(xy)
dat = colour.XYZ_to_sRGB(xyz)
dat = colour.normalise_maximum(dat, axis=-1)
 
# now make an alpha/transparency mask to hide the background
# and flip u,v axes because of column-major symantics
alpha = np.ones((samples, samples)) # * wvl_mask
dat = np.swapaxes(np.dstack((dat, alpha)), 0, 1)
 
# lastly, duplicate the lowest wavelength so that the boundary line is closed
wvl_uv = np.vstack((wvl_uv, wvl_uv[0,:]))
 
fig, ax = plt.subplots(figsize=(5,5))
ax.imshow(dat,
         extent=[xlim[0], xlim[1], ylim[0], ylim[1]],
         interpolation='None',
         origin='lower')
 
ax.set(xlim=(0, 0.7), xlabel='CIE u\'',
       ylim=(0, 0.7), ylabel='CIE v\'')
ax.plot(wvl_uv[:,0], wvl_uv[:,1], c='0', lw=3)
plt.show()

Method 2

colour.plottingを用いる方法。

import colour.plotting as cpl
 
cpl.plot_chromaticity_diagram_CIE1976UCS(standalone=False)
cpl.render(
        standalone=True,
        bounding_box=(-0.1, 0.7, -0.05, 0.7),
        x_tighten=True,
        y_tighten=True,
        filename="CIE1976UCS_diagram.png")
plt.show()