MATLABを用いた簡易測色計の作成と色の表示

こんにちは．色覚と瞳孔について研究する学生です．

先日，SIGGRAPH Asiaで[色の変化する刺激と瞳孔で色覚検査できるかもなとかのポスター発表](https://dl.acm.org/doi/10.1145/3550082.3564161)してきました(もちろん実験や解析はMATLABを使ってます)．
で，この実験では色の輝度や色相をかなり厳密に統制しています．色の計測には分光放射輝度計というものを用いて計測して，MATLABで刺激のシミュレーションして，刺激を作って…とやっており，ここの計測が正直なところまあまあ大変で，なんかそのへんのセンサーで代用出来たらうれしいなあというモチベーションがこの記事の内容です．

内容としてはArduinoとカラーセンサー(TCS34725)を使った冬休みの工作ですが，ArduinoのライブラリなしでMATLABでI2Cを直で読んだりする事例が少なく(?)，書いてみようと思った次第です．

## コードについて
今回書いたコードはここにあります．

https://github.com/ShuftBK/MATLAB_TCS34725

## MATLAB Support Package for Arduino Hardwareは偉大
MATLABでArduinoの制御については，MATLAB Support Package for Arduino Hardwareを使えばArduino IDEで書き込みしなくても，MATLABでいつも通りコードを書いて実行するだけでいいので非常に便利です．
過去のコードベースとライブラリの依存関係を解消できないので，別の実験ではMATLABとArduinoをシリアル通信でやりとりしていますが，移すコストさえなんとかなるならサポートパッケージに頼るメリットはまあまあ大きいと思います．
今回は，MATLAB Support Package for Arduino HardwareでTCS34725というカラーセンサーからI2Cで読み書きをしてみました．

## TCS34725について

TCS34725はRGBの3色とClearの計4色を16bitで計測できるカラーセンサーです．I2Cを使ってセンサーの積分時間やゲイン調整をして，RGBCのレジスタから測定結果を読みだせます．
例の学会のあと，東京に行ったタイミングでAdafruitのものを秋月で調達しました．

https://akizukidenshi.com/catalog/g/gM-08220/

https://www.adafruit.com/product/1334

## Adafruitなら既存のライブラリがあるでしょ？
Arduino IDEで使う分にはあります．しかし，シリアル通信でやりとりすることになると思うので，ライブラリをそのまま使ってMATLABで動かすのはできないと思います．(できそうなら教えてほしいです)
一応Custom Arduino Librariesなるものが用意されているので，できなくはなさそうですが，このためにAdafruitのライブラリ実装とこのドキュメントを読み解いて書き出すのはこの記事を書く時間的に厳しかったので，紹介だけしておきます．

https://jp.mathworks.com/help/supportpkg/arduinoio/custom-arduino-libraries.html


## 実行環境(材料)
- MATLAB R2022a
- MATLAB Support Package for Arduino Hardware
- Adafruit RGB Color Sensor with IR filter and White LED - TCS34725
- Arduino Mega 2560 (Unoで十分)
- そのへんにあったブレッドボードとジャンパワイヤ
- 食卓塩の蓋 (養命酒のキャップ)

## サポートパッケージでarduinoを使うには

MATLABを開いた状態でArduinoをPCに接続するとMATLAB Support Package for Arduino Hardwareについて案内が出てきます．これに従って入れていきます．
設定についてはデフォルトのまま進めました．

https://www.mathworks.com/help/releases/R2022a/supportpkg/arduino/ug/install-support-for-arduino-hardware.html

導入すると`arduino()`という関数を使えるようになります．これで返り値として出てくるデバイスリストのデバイスは，Arduino IDEのようにコンパイルして書き込みするようなことをしなくても，MATLAB上で更新や実行を行うことができるようになります．

次に，`scanI2CBus()`に対して`arduino()`で取得したデバイスリストから，デバイスを引数として与えてやると，そのデバイスに現在接続済みのI2Cデバイスのアドレスが返ってきます(TCS34725だと0x29になる)．

そして，`device()`にデバイスとI2Cアドレスを与えれば，I2Cで通信する準備は完了です．ここで返ってきた値(ワークスペースだと`1x1 device`)を用いてread/writeをしていきます．

``` matlab:colorimetry.m
%% Initialize I2C device
% a = arduino('COM3','Mega2560','Libraries','I2C'); % COMportが分かってるならこっちのほうがいいかも
a = arduino(); % 接続しているarduinoデバイスのリストを返す
bus = scanI2CBus(a(1)); % TCS34725 Address: 0x29
dev = device(a,'I2CAddress',bus{1});
```

## TCS34725のレジスタへ書き込みと読み込みをする

実装に当たって次のサイトを参考にしました．

https://kurobekoblog.com/tcs34725_datasheet

ここでポイントなのが，コマンドアドレスとの論理和によってレジスタ指定を行います．
先に情報を読み書き込みたいレジスタにwriteして，次にread/writeをすることで内容の読み込みと書き込みが可能です．

``` matlab:colorimetry.m
%% Get TCS34725 id
Idregister = bitor(0b10000000,0x12,'uint8');
write(dev,Idregister,"uint8");
id = read(dev, 1,"uint8");
fprintf('TCS34725 ID: 0x%x\n', id);   % 0x44 = TCS34721 or TCS34725

%% Set CRGB Time
CRGBTimeregister = bitor(0b10000000,0x01,'uint8');
write(dev,CRGBTimeregister,'uint8');
write(dev,0x00,'uint8')% 256Cycle

%% Set Control Register
Controlregister = bitor(0b10000000,0x0F,'uint8');
write(dev,Controlregister,'uint8');
write(dev,0x10,'uint8')% 16xGAIN

%% Set Control Register
Enableregister = bitor(0b10000000,0x00,'uint8');
write(dev,Enableregister,'uint8');
write(dev,0b00000011,'uint8');
```

## 色の情報を取り出す

先ほどの設定と同様に，色の情報を取り出すためにwriteしてからreadします．
具体的には，0x14から8byteが色情報が格納されているアドレスなので，連続してここを読み出します．
その後，8bitで返ってきたデータをシフト演算と論理和することで，16bitのセンサーデータとして取得します．

``` matlab:colorimetry.m
%% Get ColorData
while 1
    Dataregister = bitor(0b10100000,0x14,'uint8');
    write(dev,Dataregister,'uint8');
    CRGB = read(dev, 8, "uint8");
    C = bitor(CRGB(1),bitshift(CRGB(2),8,'uint16'),'uint16');
    R = bitor(CRGB(3),bitshift(CRGB(4),8,'uint16'),'uint16');
    G = bitor(CRGB(5),bitshift(CRGB(6),8,'uint16'),'uint16');
    B = bitor(CRGB(7),bitshift(CRGB(8),8,'uint16'),'uint16');
    fprintf('C: %6d, R: %6d, G: %6d, B: %6d\n',C,R,G,B);
    pause(.25);
end
```

色がとれていれば，0.25 s ごとに次のような感じで各値が見れます．

```
C:  59535, R:  22018, G:  19972, B:  15266
C:  65535, R:  24931, G:  24421, B:  18542
C:  65535, R:  24931, G:  24421, B:  18542
C:  65535, R:  25745, G:  26041, B:  19768
C:  65535, R:  25745, G:  26041, B:  19768
C:  65535, R:  25118, G:  24987, B:  18969
```

## この値ってどうやって使えばいいか？

使い方としては愚直にRGBが8bitではなく16bitになったとして使うこともできますし，Adafruitのライブラリの中には，先ほど取得したRGB値に対してC値で割ったものに255.0をかけてやる処理が書かれており，こうした工夫をしたうえで使うこともできます．

色として正しく出力されてそうかを見るためにこの2つを実装して，figureにプロットしてみました．setでFaceColorを都度更新しているので，あまり良い処理ではない気がしますが次のようにしています．(賢いやり方があれば教えてほしいです)

``` matlab:colorimetry.m
%% Initialize figure plot area
f = figure();
box on;
area1 = fill([0 1 1 0],[0 0 1 1], 'k');  % make black area
ax = gca;
pbaspect([1 1 1]);
ax.XTickLabel = cell(size(ax.XTickLabel));
ax.YTickLabel = cell(size(ax.YTickLabel));
ax.TickLength= [0 0];

f = figure();
box on;
area2 = fill([0 1 1 0],[0 0 1 1], 'k');  % make black area
ax = gca;
pbaspect([1 1 1]);
ax.XTickLabel = cell(size(ax.XTickLabel));
ax.YTickLabel = cell(size(ax.YTickLabel));
ax.TickLength= [0 0];

%% Get ColorData
while 1
    Dataregister = bitor(0b10100000,0x14,'uint8');
    write(dev,Dataregister,'uint8');
    CRGB = read(dev, 8, "uint8");
    C = bitor(CRGB(1),bitshift(CRGB(2),8,'uint16'),'uint16');
    R = bitor(CRGB(3),bitshift(CRGB(4),8,'uint16'),'uint16');
    G = bitor(CRGB(5),bitshift(CRGB(6),8,'uint16'),'uint16');
    B = bitor(CRGB(7),bitshift(CRGB(8),8,'uint16'),'uint16');
    r = R / C * 255.0;
    g = G / C * 255.0;
    b = B / C * 255.0;
    fprintf('C: %6d, R: %6d, G: %6d, B: %6d\n',C,R,G,B);
    set(area2,'FaceColor',[R/65535 G/65535 B/65535]);
    set(area1,'FaceColor',[r/255 g/255 b/255]);
    pause(.25);
end
```

https://twitter.com/nk_Mackerel/status/1605539342671581184?s=20&t=V_jTC4W3f8rFcpIq4Jlj3Q

画面中の左のfigureがRGBをCで割って255かけたもので，右がRGBをそれぞれ65535で割ったものです．
結果として，少なくともRGBの三原色に近い物体の色は特に難しいことをしなくてもだいたいいい感じに取れてるのがわかるかと思います．

## なんで蓋とかつかってるの
その辺にあって加工が楽なのと，測色面に対してちょうどいい高さなので採用しました．
もともと養命酒のキャップを使っていましたが，あれだと少し高さが高いため，色が真っ黒になってしまう問題がどうしても解決できなかったため，低めのこちらにしました．

https://twitter.com/nk_Mackerel/status/1605544849771565058?s=20&t=DM_EnxdnGy-iycPjOrvzjQ

この蓋についてお気づきと思いますが，黒い布を貼っています．というのも，LED光源の拡散の観点から，黒い布なしで蓋のうえで測色すると，LED光源から出た光が蓋で反射して計測値が赤っぽくなります．また，蓋なしでやると今度は周辺の環境光(蛍光灯)の影響をもろに受けて，ゲインが60の場合は全部値が最大で張り付いてしまうため，調整する必要があります．こうした点から，蓋に黒い布を貼っていますが，安く済ませるならマッキーとかで黒く塗ったり，ちょっと高くなりますが黒色無双とかの吸光率の高い塗料で塗装するといいかなと思います．
3Dプリンターで最初から黒いフィラメントでプリントするとかしたほうが，設計上のメリットが大きい(無駄に穴開けたり削ったりする必要ないし)ので3Dプリンターが欲しくなりました…

## 今後の展望

展望としてやるべきことがあるとするならば，ダークキャリブレーション考える必要がある点や，白色LEDについて考慮していないことやセンサーの個体値を含めた校正を行う必要があるかなと思います．

ダークキャリブレーションや白色LED光源については，それぞれ計測前にデータとして取得し，差分を取るなどするのがいいかなと思います．センサーの仕様についてまだわかっていないところがあるため，アプリケーションノートについて読み込んだうえで実装書きたいですね．

校正については`displayColorPatch()`という関数があり，こちらはカラーチャートの映った画像を用いて色差を算出していますが，カラーチャートをセンサーで測色し，`displayColorPatch()`に適応できる形式にしてやれば，その誤差から校正できないかなと考えています．

## まとめ

今のところ最初に書いていたモチベーションを満たせるレベルではありませんが，校正次第でそこそこよさそうになるのかなという気はします．久しぶりのセンサーを使った工作ということもあり，楽しんでやれたのがよかったですね．個人的にはもう少ししっかりコードを書きたいという気持ちがあるので，気が向いたらカスタムライブラリを作ってもいいなと思います．