カラーセンサー(S11059-02DT)を使って基本16色の識別に挑戦
1.基本16色
ここで扱う16色は、HTMLで定義されている基本16色とします。WEBブラウザ上でのRGB成分は下表のとおりですが、これをインクジェットプリンターで印刷して使いますのでカラーセンサーで検出される成分はかなり違うものとなります。
色 名 | R,G,B成分 | 色 |
白 | 255, 255, 255 | 0 |
シルバー | 192, 192, 192 | 1 |
グレー | 192, 192, 192 | 2 |
黒 | 192, 192, 192 | 3 |
|
色 名 | R,G,B成分 | 色 |
赤 | 192, 192, 192 | 4 |
マローン | 192, 192, 192 | 5 |
黄色 | 192, 192, 192 | 6 |
オリーブ | 192, 192, 192 | 7 |
|
色 名 | R,G,B成分 | 色 |
ライム | 192, 192, 192 | 8 |
緑 | 192, 192, 192 | 9 |
アクア | 192, 192, 192 | 10 |
ティアル | 192, 192, 192 | 11 |
|
色 名 | R,G,B成分 | 色 |
青 | 192, 192, 192 | 12 |
ネイビー | 192, 192, 192 | 13 |
フクシャ | 192, 192, 192 | 14 |
パープル | 192, 192, 192 | 15 |
|
2.回路図
今回はフルカラーLEDではなく普通の白色LED(OSPW5111A-Z3)を使います。これをOFF/ONさせながらセンサーを2度読みし、その差分をとって色を識別することで外来光の影響を極力低減させます。センサーの制御とPCとの通信用にArduino-UNOを使います。
Arduino_and_color_sensor(S11059-02)_ver21
3.ARDUINO用プログラム(スケッチ)
スケッチは、メインとなる「Color_sens_1ch_202012.iso」と、I2C通信用の「i2c_pro.iso」の2つで構成されています。
D/L_files: Color_sens_Arduino_202101.zip
「Color_sens_1ch_202012.iso」
(1)void setup()
ポート方向の設定は、CPU内のDDRB,DDRC,DDRDに直接設定します。シリアル通信を開始した後、EEPROMに書き込んである参照用の色データを読み込んで配列にセットします。カラーセンサーの動作モードは、2.8msをタイムベースとするマニュアルモードとしますので、あらかじめセンサー内部のマニュアルタイミングレジスタ(アドレス01,02)に乗算値を書き込んでおきます。ここでは初期値として10を書き込みますので、積分時間は2.8×10=28msとなります。
DDRB = B00111111; // b0-b5:output
DDRC = B00111111; // b0-b5:output
DDRD = B11111100; // b2-b7:output
Serial.begin(9600);
for(col=0; col<16; col++){ //read 16-color reference data from EEP-ROM
dl = EEPROM.read(col*8);
dh = EEPROM.read(col*8+1);
d = dh*256 + dl;
col_Ref[col].R = d;
:
}
integ_mlti_val = 10; //default value=10, base integration time=2.8ms.
byte ack = integ_wr(integ_mlti_val); //set the integration time/color to 2.8*10=28ms
(2)byte integ_wr(byte dt)
積分時間を決定する乗算値を実際に書き込みます。センサーの取説に書いてあるとおりにコントロールレジスタ(00番地)に書き込んでからアドレス01、02番地に乗算値を書いています。プログラムとしては、I2Cアドレスコール(0x2A:センサー内部)から順にマニュアルどおりに書き込むだけですが、注意する点は、I2Cアドレスコールはセンサー内部では0x2Aですが、これらはBit1からBit7に配置されていて、bit0がW(0)/R(1)コマンドビットとなるので、Arduinoから送るデータとしては、0x54又は0x55となることです。
i2c_start();
ack =i2c_write_byte(0x54); //I2C address call
ack|=i2c_write_byte(0x00); //address(0x00):control reg.
ack|=i2c_write_byte(0b11101101); //ADC_reset(b7), sleep(b6), high gain(b3), manual mode(b2), 01(b1,b0)=2.8ms
ack|=i2c_write_byte(0x00); // multiple value(MSB)
ack|=i2c_write_byte(dt); // maltiple value(LSB)
i2c_stop();
(3)byte get_color()
被測定物の色をR、G、B、IRの各成分値として読み取ります。I2CをスタートしセンサーのADCをリセット、その後、再スタートし、ADCの動作をマニュアルモードで開始させます。そして、センサーが積分している間待ちます。積分時間はPC側から変更できるようにしたので、その値(integ_mlti_val)に合わせて変更する必要があります。この積分が終わるまでの待ち時間が少ないとデータが更新されないことがあります。
次に、センサーからR、G、B、IRのデータ8バイトを読み込み、それを16bitデータに変換してから、col_X.R、G、B、IRに保存します。戻り値が0でなければエラーがあったことになります。
i2c_start();
ack=i2c_write_byte(0x54); // address call
ack|=i2c_write_byte(0x00); // address(0x00):control reg.
ack|=i2c_write_byte(0b10001101); //ADC_reset, high gain(b3=1), manual mode(b2=1), 01=2.8ms
i2c_start(); // Restart
ack|=i2c_write_byte(0x54); // address call
ack|=i2c_write_byte(0x00); // address(0x00):control reg.
ack|=i2c_write_byte(0b00001101); // ADC_operation, high gain(b3=1), manual mode(b2=1), 01=2.8ms
i2c_stop();
if(ack==1)return 2; //I2C write err.
for (i=0; i<integ_mlti_val; i++){ // waiting time, depends on integ_mlti_val(10 to 100).
delay(18); //more than 11.2ms(2.8ms*4)
}
i2c_start();
ack|=i2c_write_byte(0x54); // address call
ack|=i2c_write_byte(0x03); // data reg. address(0x03)
i2c_start(); // Restart
ack|=i2c_write_byte(0x55); // read mode
for(i=0; i<7; i++){
RGB_dat[i]=i2c_read_byte(0); // ack.=on read data
}
RGB_dat[i]=i2c_read_byte(1); // ack.=off, read last data
i2c_stop();
col_X.R = RGB_dat[0]*256+RGB_dat[1];
col_X.G = RGB_dat[2]*256+RGB_dat[3];
col_X.B = RGB_dat[4]*256+RGB_dat[5];
col_X.IR = RGB_dat[6]*256+RGB_dat[7];
return ack; //ack=1 I2C read err.
(4)byte detect_col()
センサーが読み取った色とあらかじめ参照用として保存された色を、それぞれR,G,B,IR毎に比較し、最も近い色をその番号(0~15)で回答します。
float result[16], max_d;
float Rx, Gx, Bx, IRx, Sx, Rr, Gr, Br, IRr, Sr, dR, dG, dB, dIR;
for(i=0; i<16; i++)result[i]=0; //clear result[]
Rx=col_X.R;
Gx=col_X.G;
Bx=col_X.B;
IRx=col_X.IR;
Sx=Rx+Gx+Bx+IRx;
Rx=(Rx/Sx)*100;
Gx=(Gx/Sx)*100;
Bx=(Bx/Sx)*100;
IRx=(IRx/Sx)*100;
for(i=0; i<16; i++){
Rr=col_Ref[i].R;
Gr=col_Ref[i].G;
Br=col_Ref[i].B;
IRr=col_Ref[i].IR;
Sr=Rr+Gr+Br+IRr;
Rr=(Rr/Sr)*100;
Gr=(Gr/Sr)*100;
Br=(Br/Sr)*100;
IRr=(IRr/Sr)*100;
if(Rx<Rr){dR=Rr-Rx;}else{dR=Rx-Rr;}
if(Gx<Gr){dG=Gr-Gx;}else{dG=Gx-Gr;}
if(Bx<Br){dB=Br-Bx;}else{dB=Bx-Br;}
if(IRx<IRr){dIR=IRr-IRx;}else{dIR=IRx-IRr;}
result[i]=(100-(dR+dG+dB+dIR));
}
max_d = 0;
x = 0xff;
for(i=0; i<16; i++){
if(result[i]>max_d){
max_d=result[i];
x = i;
}
}
return x; //x=0-15:ok, x=0xff(255):err..
被測定物から得られたR,G,B,IRの合計値をSxとし、Sxに対する各色成分の比率(%)をRx、Gx、Bx、IRxとして求めます。
同様にして、あらかじめ参照用として保存してある色番号iの色成分からそれぞれの比率(%)を、Rr、Gr、Br、IRrを求めます。
両者の色成分比率の差分を、dR、dG、dB、dIRとすると、差分の合計(dR+dG+dB+dIR)が小さいものほど、色が似ていることになります。
これを、逆に大きい(100に近い)ものほど、色が似ているとするために、100-(dR+dG+dB+dIR)として、result[色番号i]に保存します。
この作業を参照用の色番号の数だけ繰り返し、最後に、result[i]のなかで最も大きな値となっているものを探し、その色番号iを戻り値として回答します。
(5)void loop()
このメインループの中では、PCから何らかのデータを受信したかどうかチェックし、もし、受信していれば次の作業を実行するものです。
・受信データアレイ及びチェックサム(chk_sum)をクリアします。
・PCから送信されてくるデータは3バイト又は11バイトなので、それらを全て受信する時間(少し余裕をみて25ms)だけ待ちます。
・FORループにより、受信バッファから1バイトずつ受信データアレイ(rcv_dt[])へ読み込みます。また同時に、0~9バイト目までを加算し、チェックサム(chk_sum)を生成します。
・全て読み終わったらFORループを抜けて次の①へ進みます。この時点で、rcv_dt[0,1,2]又は、rcv_dt[0~10]には、PCから送信されたデータが読み込まれています。
if(Serial.available()>0){ // 受信したデータが存在するか?
for(int i=0; i<16; i++) rcv_dt[i]=0;
chk_sum=0;
delay(25); //** adj. waiting time for 9byte-data receive all.
for(int i=0; i<16; i++){
d=Serial.read();
if(d==-1)break; //receive all?
rcv_dt[i]=d;
if(i<10)chk_sum+=rcv_dt[i]; //check sum
}
(PCから送信されるコマンドは次の3種類)
① 第1、第2バイト目が、0xaa,00の場合 : GET_COLOR and DETECT_COLORコマンド
まず、PC側とArduinoに書き込まれている積分時間が等しいかチェックします。等しくなければPC側に合わせます。次に白色LEDを点灯させずに、get_color()を実行して色データを読み取ります(col_Xp.R,G,B,IR)。その直後に白色LEDを発光させた状態で再び読み取ります。後者と前者の差をとって被測定物からの色データ(col_X.R,G,B,IR)とします。色データはそれぞれ16ビットなので、これを2バイトデータに変換しながら、1バイトづつ、R(下位バイト、上位バイト),G,B,IRの順で合計8バイト送信します。次に、detect_col()を使って最も近い色を求め、その色番号(0~15)を送信し、最後の10バイト目にチェックサム(chk_sum)を送信して終了します。
if((rcv_dt[0]==0xaa)&&(rcv_dt[1]==0)){
if(rcv_dt[2]!=integ_mlti_val){ // if not, rewrite multiple value of the integration time
integ_mlti_val=rcv_dt[2];
d=integ_wr(integ_mlti_val);
}
d=get_color(); //get the data while LED off.
col_Xp.R=col_X.R; col_Xp.G=col_X.G; col_Xp.B=col_X.B; col_Xp.IR=col_X.IR;
digitalWrite(12,HIGH); //LED ON
d|=get_color(); //get the data while LED on.
digitalWrite(12,LOW); //LED OFF
if (col_X.R>col_Xp.R){col_X.R-=col_Xp.R;}else{col_X.R=0;} //difference is the data.
if (col_X.G>col_Xp.G){col_X.G-=col_Xp.G;}else{col_X.G=0;}
if (col_X.B>col_Xp.B){col_X.B-=col_Xp.B;}else{col_X.B=0;}
if (col_X.IR>col_Xp.IR){col_X.IR-=col_Xp.IR;}else{col_X.IR=0;}
if(d==0){ //no err.?
chk_sum=0;
chk_sum+=col_X.R; Serial.write(col_X.R); chk_sum+=(col_X.R/256); Serial.write(col_X.R/256);
chk_sum+=col_X.G; Serial.write(col_X.G); chk_sum+=(col_X.G/256); Serial.write(col_X.G/256);
chk_sum+=col_X.B; Serial.write(col_X.B); chk_sum+=(col_X.B/256); Serial.write(col_X.B/256);
chk_sum+=col_X.IR; Serial.write(col_X.IR); chk_sum+=(col_X.IR/256); Serial.write(col_X.IR/256);
col=detect_col();
chk_sum+=col;
Serial.write(col); //0-15=ok, 255=err.
Serial.write(chk_sum);
}
② 第1、第2バイト目が、0xcc,00の場合 : 積分時間を決定する乗算値を更新するコマンド
PC側で、積分時間を変更した時に送信されます。受信データアレイのrcv_dt[2]に受信されますので、この値に更新することにより、2.8ms~280msの範囲で変更することができます。
if((rcv_dt[0]==0xcc)&&(rcv_dt[1]==0)){
integ_mlti_val=rcv_dt[2]; //value 1 to 100, 2.8ms to 280ms per color
d = integ_wr(integ_mlti_val);
if(d==0){Serial.write("o");}else{Serial.write("n");}
}
③ 第1バイト目が、0xbbの場合 : 色データを比較参照するための基準となる色データの送信コマンド
PC側から0xbbのコマンドに続いて、色番号(0~15)、色データ(8バイト)、チェックサム(1バイト)が送信されます。従って、Arduino側では、PC側で計算したチェックサム(rcv_dt[10])と受信の際に計算した値を比較して、一致した場合にはこれらのデータを色番号に応じたEEPROMアドレスのところへ格納します。
if((rcv_dt[0]==0xbb)&&(rcv_dt[10]==chk_sum)){
col = rcv_dt[1];
col_Ref[col].R = rcv_dt[3]*256+rcv_dt[2];
col_Ref[col].G = rcv_dt[5]*256+rcv_dt[4];
col_Ref[col].B = rcv_dt[7]*256+rcv_dt[6];
col_Ref[col].IR = rcv_dt[9]*256+rcv_dt[8];
EEPROM.write((col*8),rcv_dt[2]); EEPROM.write((col*8+1),rcv_dt[3]); //write(adrs,value)
EEPROM.write((col*8+2),rcv_dt[4]); EEPROM.write((col*8+3),rcv_dt[5]); //write(adrs,value)
EEPROM.write((col*8+4),rcv_dt[6]); EEPROM.write((col*8+5),rcv_dt[7]); //write(adrs,value)
EEPROM.write((col*8+6),rcv_dt[8]); EEPROM.write((col*8+7),rcv_dt[9]); //write(adrs,value)
Serial.write("o"); //ok.
}
「i2c_pro.iso」
I2C通信を任意のポートに割り当てて行うための自作プログラムです。必要最低限のスタート、ストップ、1バイト書き込み、1バイト読み込みしかありませんが、センサーとの通信には十分間に合います。プログラム自体はI2Cの手順どおりに、1、0を出力又は読み込みを行うだけの単純なものです。
4.PC側プログラム(Windowsフォームアプリ)
今回は、Visual Studio Comunity 2019 C#ツールを使用しました。操作画面は次のように簡単なもので、シリアル通信の接続/切断(Connect/Disconnect)、色の読み取りボタン(Get Color)、読み取った色を色番号を指定してEEPROMへ書き込むボタン(Write EEPROM)、そして積分時間を指示するTrackBarがあるだけです。
D/L_files: Color_sens_C#_202101.zip
基本16色をA4用紙にインクジェットプリンターで印刷しました。
それをこんなふうに4色づつに切り分けて、それを折って箱型にして使います。
カラーセンサー回路はブレッドボード上に作成しArduinoと結線します。白色LEDは黒のビニールテープを巻いてセンサーへ直接入る光を低減しました。これに厚紙で作ったボックス(高さ4cm)を被せ、その上に測定物を載せます。
PCとArduinoをUSBケーブルで繋いだ状態で、PC側のC#で作成したプログラムを実行すると下の写真のような操作パネルが現れます。シリアルポートを確認し、"connect"ボタンを押してAruduinoと通信を開始します。その後、"Get Color"ボタンをクリックすると、Arduino側で白色LEDを点灯させて色成分を読み取りR、G、B、IRのデータと、あらかじめEEPROMへ格納した基準データとを比較して、最も色が似ている色番号が送信されてきます。PC側ではそれらのデータに付加されたチェックサムを確認しデータに誤りがなければ表示します。下の例は、積分時間を28ms(初期値)として測定したときのもので、測定中の色は14番(フクシャ)と識別しています。6番の下は14番ですので正しい判定です。
下の画像は、積分時間を変えて測定した場合です。約100msにした場合と、最大の280msにしたときのデータを表示しています。
基準となる色データのEEPROMへの書き込みは、下の画像のように積分時間(ここでは103.6ms)を決めてから、"Get Color"を実行してデータを取得し、その色に設定したい色番号をRef.Color No. のテキストボックスのところへ入力(0~15)し、最後に"Write EEPROM"ボタンをクリックして書き込みます。
5.実験結果
最初は白色LEDを点灯させて色成分を読み取るだけの一度読みで実験したのですが、外部からの光を受けてしまって間違えることが多く、なんだこんなものか、、と思ったのですが、考えてみれば当たり前のことですね。そこで、センサーとLEDの部分だけ内部を黒くした円筒を作って完璧に囲って再実験したところ、、なんと素晴らしいこと!百発百中です。まあ、これも当たり前ですね。でも、これじゃー、扱いにくいのですね。そこで、始めに白色LEDをOFFとして外来光のみを測定し、直後にLEDをONにして被測定物からの反射光+外来光を測定し、その差分をとって識別するようにしたものです。結果はなかなか良好で、白とシルバー、シルバーとグレーは(グレースケールは色成分比率が同じなので)間違えることもありますが、それ以外は室内での使用であればかなり高い精度(百発百中レベル)で識別可能と思われます。