【きれい】RGB色空間の断面図アニメーションをPython+matplotlib+ffmpegで作成した

# できたもの

![RGB_CS_cross-section.gif](https://qiita-image-store.s3.amazonaws.com/0/150194/cc5277fd-113e-7b15-1bd7-7b6935bd70ab.gif)

gifだとどうしても色数が減ってしまうので荒いです。動画版をyoutubeにアップしたのでぜひどうぞ。R軸方向・G軸方向・B軸方向それぞれの断面図アニメーションになってます: **[youtubeへ](https://youtu.be/QvfUVwOBCLM)**




# 解説

画像の色情報は（赤成分, 緑成分, 青成分）の組で表現することができます。RGBというやつです。
それぞれの成分は0から255の256階調のいずれかで表すのが普通です。
例:

- `(0,0,0)` ... ぜんぶ0。黒
- `(255,255,255)` ... ぜんぶ255。<font color="WhiteSmoke">白</font>
- `(255,0,0)`... R成分だけ255。<font color="Red">真っ赤</font>
- `(0,128,0)`... G成分だけ128。<font color="LimeGreen">鈍い緑</font>
- `(255,255,0)`... R成分とG成分が255。<font color="Yellow">真っ黄色</font>

R軸・G軸・B軸の3次元空間に各色を置くと、カラフルな豆腐として描画することができます。
こういう色の表し方を **RGB色空間** と言います:

![RGBCS_exp02.png](https://qiita-image-store.s3.amazonaws.com/0/150194/e0fc97df-28c3-6429-98a6-85f319ca384f.png)

この図では豆腐の表面の色だけが見えていますが、もちろん中身もカラフルです。断面図を見たい! と思ったのですが、"RGB色空間 断面図"とかでググっても見つからないので作りました。


# コード

軸方向にスライドしつつ断面図を1枚ずつ作る → それらの静止画を動画としてまとめる という2段構えです。

## 断面図を得る

Pythonとmatplotlibでやりました[^1]。Pythonのバージョンは3.6.6、matplotlibのバージョンは2.2.2。

[^1]: matplotlibを使ったのはあまりいい選択ではなかった。[matplotlibは3次元の描画があまり得意でないようで](https://matplotlib.org/mpl_toolkits/mplot3d/faq.html)、確かに、出力される画像の左側、「いまここの断面を表示している」ことを示す平面が豆腐の手前か奥にあるようにしか描画されない。

```python

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# R=255の平面, G=255の平面, B=255の平面を色付きで描画することで3DのRGB空間のイメージとする。

# 各平面の準備。0~255を1x1ずつ区切り、表示の際に各区画に色を振る
r_X = np.full((256, 256), 255)
r_y = np.linspace(0, 255, 256)
r_z = np.linspace(0, 255, 256)
r_Y, r_Z = np.meshgrid(r_y, r_z) 

g_Y = np.full((256, 256), 255)
g_x = np.linspace(0, 255, 256)
g_z = np.linspace(0, 255, 256)
g_Z, g_X = np.meshgrid(g_z, g_x) 

b_Z = np.full((256, 256), 255)
b_x = np.linspace(0, 255, 256)
b_y = np.linspace(0, 255, 256)
b_X, b_Y = np.meshgrid(b_x, b_y) 

# 平面に振る色データを作成する。
# plot_surfaceに渡す平面のグリッド数と同じshape(=256x256)に、
# RGBをそれぞれはめ込んで256x256x3にする。
# facecolorsの制約上、RGBは0から1の範囲で指定する必要がある。
r_colors = np.zeros((256, 256, 3))
for i in range(256):
    for j in range(256):
        r_colors.itemset((j,i,0),1.0)
        r_colors.itemset((j,i,1),i/255) 
        r_colors.itemset((j,i,2),j/255) 

g_colors = np.zeros((256, 256, 3))
for i in range(256):
    for j in range(256):
        g_colors.itemset((i,j,0),i/255)
        g_colors.itemset((i,j,1),1.0)
        g_colors.itemset((i,j,2),j/255)

b_colors = np.zeros((256, 256, 3))
for i in range(256):
    for j in range(256):
        b_colors.itemset((j,i,0),i/255)
        b_colors.itemset((j,i,1),j/255) 
        b_colors.itemset((j,i,2),1) 

# 図の作成
space_margin=60
gp_margin=80
gp_fp = np.linspace(0-gp_margin, 255+gp_margin, 2)
gp_fp1, gp_fp2 = np.meshgrid(gp_fp, gp_fp) 

for pos in range(256):
    
    # 左側の図: RGB色空間とどこを表示しているか示す平面（guide_plain）
    gp_vp = np.full((2, 2), pos-10) #10とか15は見た目ズレの補正
    gp_vpZ = np.full((2, 2), pos-15)
    
    fig = plt.figure(figsize=(16,9))
    ax1 = fig.add_subplot(121, projection='3d')
    ax1.set_xlabel("R", fontsize=20)
    ax1.set_ylabel("G", fontsize=20)
    ax1.set_zlabel("B", fontsize=20)
    ax1.set_xlim(0-space_margin, 255+space_margin)
    ax1.set_ylim(0-space_margin, 255+space_margin)
    ax1.set_zlim(0-space_margin, 255+space_margin)
    ax1.plot_surface(gp_vp, gp_fp1, gp_fp2, alpha=0.1, linewidth=0.5, edgecolors='black') #R軸
    #ax1.plot_surface(gp_fp1, gp_vp, gp_fp2, alpha=0.1, linewidth=0.5, edgecolors='black') #G軸
    #ax1.plot_surface(gp_fp1, gp_fp2, gp_vpZ, alpha=0.1, linewidth=0.5, edgecolors='black') #B軸
    ax1.plot_surface(r_X, r_Y, r_Z, facecolors=r_colors, shade=False)
    ax1.plot_surface(g_X, g_Y, g_Z, facecolors=g_colors, shade=False)
    ax1.plot_surface(b_X, b_Y, b_Z, facecolors=b_colors, shade=False)
    ax1.view_init(45.0, 45.0)

    # 右側の図: 断面図
    cross_section = np.zeros((256,256,3), np.uint8)
    cross_section[:,:] = [pos, 0, 0] #R軸
    #cross_section[:,:] = [0, pos, 0] #G軸
    #cross_section[:,:] = [0, 0, pos] #B軸

    for i in range(256):
        for j in range(256):
            cross_section.itemset((i,j,1),i) #R軸
            cross_section.itemset((i,j,2),j) #R軸
            #cross_section.itemset((i,j,0),i) #G軸
            #cross_section.itemset((i,j,2),j) #G軸
            #cross_section.itemset((i,j,0),i) #B軸
            #cross_section.itemset((i,j,1),j) #B軸
    
    # 左側の図と比較しやすくするために回転など
    cross_section = np.rot90(cross_section, k=1) #R軸
    #cross_section = cross_section.transpose(1, 0, 2) #G軸
    #cross_section = np.rot90(cross_section, k=2) #G軸
    #cross_section = np.rot90(cross_section, k=-1) #B軸

    ax2 = fig.add_subplot(122)
    ax2.tick_params(labelbottom=False, bottom=False, labelleft=False, left=False)
    ax2.imshow(cross_section, vmin = 0, vmax = 255)
    ax2.set_title("cross-section view: R={0}".format(pos), fontsize=20) #R軸
    #ax2.set_title("cross-section view: G={0}".format(pos), fontsize=20) #G軸
    #ax2.set_title("cross-section view: B={0}".format(pos), fontsize=20) #B軸
    
    plt.savefig("RGB_color_space" + '{:0>3}'.format(pos) + ".png", dpi=300)
    plt.close()
```

実行するとR軸方向の断面図がR=0からR=255まで256枚 出てきます。G軸方向・B軸方向の断面図を作る場合は、コード中で"#G軸"とか"#B軸"と書いてある行のコメントアウトを外します。

## 動画化

ffmpegを使って、上で得た連番静止画を動画に変換します。ffmpegのバージョンは2.8.14。

```
$ ffmpeg -framerate 30 -i RGB_color_space%03d.png -vcodec libx264 -b 2500k -an -pix_fmt yuv420p Rdir.mp4
```

## おまけ: 軸とかすべて消すと超きれい

`ax.set_axis_off()` を追加すると軸やら何やら全部消えます。めっちゃきれい。食べたい。
![RGB_color_space_base_nogrid.png](https://qiita-image-store.s3.amazonaws.com/0/150194/b22f18d8-b744-3f0f-5e8d-6b2af00d0ad2.png)