怠け者による Julia言語 におけるロゴの色を取得と表示方法

# 本日は

Julia のアドベントカレンダー $N=7$ 日目です．

https://qiita.com/advent-calendar/2023/julia

Qiita がダークモードを対応しておりとても快適です．うれちぃ（嬉しい）

アドベントカレンダー4日目を読みました．色々ロゴを作成されておりセンスが光ります．

https://zenn.dev/hyrodium/articles/ff22c9773ca828

さて，今日の話題はロゴの色をプログラムで取得する方法の Tips について触れます．それだけです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/d1e3c8c5-2b0e-f84c-c68b-de655ab4813c.png)

(上記画像は https://github.com/JuliaLang/julia-logo-graphics から引用)

## 手動で取得

例えば赤色は次のように手動で取得できます．

```julia
julia> using ColorTypes; julia_red = RGB(0.796, 0.235, 0.2)
```

`RGB` 構造体は ColorTypes パッケージにて定義されています．

https://github.com/JuliaGraphics/ColorTypes.jl/blob/808a60f1f2b6aedf58be3be787e589a9f375a62e/src/types.jl#L96-L108

```julia
"""
`RGB` is the standard Red-Green-Blue (sRGB) colorspace.  Values of the
individual color channels range from 0 (black) to 1 (saturated). If
you want "Integer" storage types (e.g., 255 for full color), use `N0f8(1)`
instead (see FixedPointNumbers).
"""
struct RGB{T<:Fractional} <: AbstractRGB{T}
    r::T # Red [0,1]
    g::T # Green [0,1]
    b::T # Blue [0,1]

    RGB{T}(r::T, g::T, b::T) where {T <: Fractional} = new{T}(r, g, b)
end
```

ただ，`0.796` とかの数字を覚えられません．もうちょっと楽をしたいです．

## Colors.jl の定数を使う

Colors.jl パッケージにある `JULIA_LOGO_COLORS` を使うと数値をハードコーディングせずに済みます．

https://github.com/JuliaGraphics/Colors.jl/blob/f0a508bce74a3c6a15bb01a5ab52bc9b9ba81f73/src/Colors.jl#L36-L46

```julia
"""
Colors used in the Julia logo as a `NamedTuple`.

The keys are approximate descriptions of the hue and do not include black.

Not exported, use as `JULIA_LOGO_COLORS.red` etc.
"""
const JULIA_LOGO_COLORS = (red = RGB{N0f8}(0.796, 0.235, 0.2),  # colorant"#cb3c33" blocks precompilation
                           green = RGB{N0f8}(0.22, 0.596, 0.149),
                           blue = RGB{N0f8}(0.251, 0.388, 0.847),
                           purple = RGB{N0f8}(0.584, 0.345, 0.698))
```

Julia の REPL では次のようになります．

```julia
julia> using Colors

julia> julia_color = Colors.JULIA_LOGO_COLORS.red
RGB{N0f8}(0.796,0.235,0.2)
```

ここで `N0f8` は FixedPointNumbers.jl で定義されている型であり固定小数点で格納されています．ざっくり言えば $0$ から $2^8-1=255$ の整数値を $255$ で割って $[0, 1] \subset \mathbb{R}$ へマッピングしたデータと思えば良いです．NumPy ユーザは事前に下記の計算をしておくと良いでしょう．

```python
iPython 3.11.6 (main, Oct 31 2023, 11:54:48) [Clang 14.0.3 (clang-1403.0.22.14.1)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.18.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import numpy as np

In [2]: nv = np.array([203,60,51]).astype(np.uint8)/255

In [3]: 255 * nv
Out[3]: array([203.,  60.,  51.])
```

## 16進数カラーコード として取得

`#CB3C33` らしいので確かめてみましょう．下記の「RGBと16進数カラーコードの相互変換ツール」を利用します．

https://www.peko-step.com/tool/tfcolor.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/f4ce8269-6732-9fe7-c019-ad7242ee6054.png)

`R = 203, G = 60, B = 51` ですね．念のため下記のようにREPLでも確かめます

```julia
julia> UInt8(203)
0xcb

julia> UInt8(60)
0x3c

julia> UInt8(51)
0x33
```

ここで `RGB{N0f8}(0.796,0.235,0.2)` との関係を見ておきましょう．


```julia
julia> using ImageCore
julia> chv = channelview([RGB{N0f8}(0.796,0.235,0.2)])
3×1 reinterpret(reshape, N0f8, ::Array{RGB{N0f8},1}) with eltype N0f8:
 0.796N0f8
 0.235N0f8
 0.2N0f8

julia> reinterpret(UInt8, chv)
3×1 reinterpret(UInt8, reinterpret(reshape, N0f8, ::Array{RGB{N0f8},1})):
 0xcb
 0x3c
 0x33
```

もっと素朴に書くと次のようになります:

```julia
julia> using FixedPointNumbers: N0f8
julia> using ColorTypes: RGB
julia> chv = reinterpret(N0f8, [RGB{N0f8}(0.796,0.235,0.2)])
3-element reinterpret(N0f8, ::Array{RGB{N0f8},1}):
 0.796N0f8
 0.235N0f8
 0.2N0f8
 julia> reinterpret(UInt8, chv)
3-element reinterpret(UInt8, ::Array{RGB{N0f8},1}):
 0xcb
 0x3c
 0x33
```

`Int` でもう少し直感的な表記にします．
 
```julia
julia> Int.(reinterpret(UInt8, chv))
3-element Vector{Int64}:
 203
  60
  51
```

外部ツールで得られた `R = 203, G = 60, B = 51` と一致しましたね．

### 逆方向に

今度は `R = 203, G = 60, B = 51` から `RGB{N0f8}(0.796,0.235,0.2)` を作ります．

```julia
julia> using ImageCore

julia> d = [0xcb, 0x3c, 0x33]
3-element Vector{UInt8}:
 0xcb
 0x3c
 0x33

julia> _julia_red = colorview(RGB, normedview(N0f8, d))
0-dimensional reinterpret(reshape, RGB{N0f8}, reinterpret(N0f8, ::Vector{UInt8})) with eltype RGB{N0f8}:
RGB{N0f8}(0.796,0.235,0.2)

julia> # _julia_red は 0 次元配列として得られる．

julia> julia_red = _julia_red[begin]
RGB{N0f8}(0.796,0.235,0.2)

julia> using Colors

julia> Colors.JULIA_LOGO_COLORS.red == julia_red
true
```

`colorview(RGB, normedview(N0f8, d))` の部分は下記のようにしても良いです．

```julia
julia> reinterpret(RGB{N0f8}, normedview(N0f8, d))[begin]
RGB{N0f8}(0.796,0.235,0.2)
```

#### あまり気にしない

これまで `reinterpret` がいっぱい出てきました．バイナリデータをどう解釈するかを決めるものです．実際に機械学習などのハイレベルな画像タスクをする場合は「まぁこんなものがある」という程度の理解で良いです．あまり気にせず `normedview, channelview, colorview` を組み合わせて `N0f8` のフォーマットと `UInt8` のフォーマットを相互に変換することが多いでしょう．`normedview, channelview, colorview` のコードを追うと実際は `reinterpret` を内部で使っていることがわかります．

なんにせよ．`RGB{N0f8}(0.796,0.235,0.2)` と 16進数カラーコード　`#cb3c33` は同じだと確認はできました．

## "nearest named color" の取得

nearest named color としてある `brown3` を取得してみましょう．NamedColors.jl なるパッケージがあるようで，これで取得できるようです．

https://github.com/JuliaGraphics/NamedColors.jl

```julia
julia> using NamedColors

julia> colorant"brown3"
RGB{N0f8}(0.804,0.2,0.2)
```

ここで "brown3" の前に `colorant` をつけることで `@colorant_str` というマクロを使用することができます．これに関しては Julia のドキュメントに書いてあります．

https://docs.julialang.org/en/v1/manual/metaprogramming/#meta-non-standard-string-literals

`@colorant_str` マクロは次のような実装になっています:

```julia
macro colorant_str(name::AbstractString)
    local col
    try
        col = parse(Colorant, name) # この部分が大事
    catch ex
        col = named_color(name)
        :($col)
    end
    :($col)
end
```

要するに今回の場合は下記のコードを実行していることとほぼ等価です.

```julia
julia> using NamedColors

julia> parse(NamedColors.Colorant, "brown3")
RGB{N0f8}(0.804,0.2,0.2)
```

上述の調査の仕方は下記のようなコードを実行することで得ることができます．興味があれば試してください．

```julia
julia> @less colorant"brown3"
```

今回は赤色で試しましたが，青，緑，紫に関しての色に関しても Julia のプログラムとして情報を取得することができます．

## 取得はできたが，もう少し直感的に表示したいぞ！

正直 `RGB{N0f8}(0.796,0.235,0.2)` と言われてもよくわからないでしょう．なんとなく R が 0.79 だから赤色の成分が強いのかな？ぐらいはわかります．人間は欲張りなので「もう少しこ〜ね？あれね？いいかんじに可視化したいっすね」と思うわけです．
    
### Sixel.jl を使う

以下は macOS + iTerm2 の環境での話になります．Julia の画像データを `sixel_encode` でターミナルの上で色情報を表示させることができます．以下がそのデモの様子です．Qiita だと mov が貼れなかったので Twitter に投稿したものを共有しましょう．

https://x.com/MathSorcerer/status/1731656217515499753?s=20

手元で動かしたい方は下記を試してください．

```julia
using Dates
using ImageInTerminal
using Replay

# https://github.com/terasakisatoshi/QuartzGetWindow.jl

using QuartzGetWindow

function takemov()
    x, y, w, h = getWindowGeometry(getActiveWindow())
    file = "$(Dates.now()).mov"
    run(`screencapture -R$(x),$(y),$(w),$(h) -v $(file)`)
end

function record()
    i1 = @deparse using Colors: JULIA_LOGO_COLORS
    i2 = "using NamedColors: @colorant_str" # これは deparse できなかった
    i3 = @deparse using Sixel: sixel_encode
    i4 = @deparse cs = JULIA_LOGO_COLORS |> collect # red, green, blue, purple
    i5 = @deparse sixel_encode(hcat(fill.(cs, 100,100)...))
    i6 = @deparse print("nearest named colors")
    i7 = @deparse cs = [colorant"brown3", colorant"royalblue", 
                        colorant"forestgreen", colorant"mediumorchid3"]
    i8 = @deparse sixel_encode(hcat(fill.(cs, 100,100)...))
    @async takemov()
    replay([i1,i2,i3,i4,i5,i6,i7,i8], use_ghostwriter=true)
    exit()
end

record()
```

ここで, QuartzGetWindow.jl は野良パッケージです．下記のリポジトリをクローンして利用してください．

https://github.com/terasakisatoshi/QuartzGetWindow.jl

#### 一応説明

```julia
cs = JULIA_LOGO_COLORS |> collect
hcat(fill.(cs, 100,100)...)
```

100x100 の解像度で赤，緑，青，紫の画像データを作ります．`hcat` で横方向に結合し幅が 400 = 4x100 の画像データを作ります．最後に`sixel_encode` によって iTerm2 など Sixel グラフィックスに対応しているターミナルの上で画像を表示できるようにデータをエンコードしています．

Replay.jl で REPL の操作を自動化しています．`@deparse` は Replay v0.5 で利用できます．(本当は Documenter.jl の仕組みを真似するべきなんだろうなぁ・・・）.

## まとめ

Julia のロゴ情報を Julia のプログラムを使って取得しました．表示方法も示しました．怠け者なのでデモスクリプトは Replay.jl で自動化しました．もっと怠けたいので今年はなんとか人生を乗り越えたいです．

以上．