どうも、シュモクザメです。
今回の記事も画像処理です!
前回、前々回と画像から物体の重心の位置を計算するコードを作りました。
今回は求めた重心の位置の画素を調べて、その画素が何色なのかを識別するプログラムを作ります。
画像における色(RGB値)についても解説するのでぜひ確認どうぞ!
前提的なもの
前提として僕はXcodeでコードを書いて、実行しています。
opencvはhomebrewからダウンロードして、xcodeからパスをつないだり必要なファイルなどをブチ込んだり、、、とバンバンに使える状態から始めています。
Xcodeにopencvを導入する方法はgoogkeで調べればいくらでもあるので、まだ導入できてない人はまずそこをお願いします。(気が向いたらこのサイトでもopencvの導入の記事を書くかも
画像における色とは?(RGB値)
いきなりですが光の三原色というものをご存知ですか?

よく見るやつですね。
カラー画像ではこの三原色を用いてさまざまな色を表現します。
その際に用いるのがRBG値です。
RBGっていうのはRed Blue Greenのことでその名の通り(赤、青、緑)のいみです。
RGB値はそれぞれの色に対応した3つのチャンネルがあり、各チャンネルは0〜255までの値を取り得ます。
この値の組み合わせで色を表現しているってことです。
これまでの記事でもカラー画像は
#define HEIGHT 480
#define WIDTH 640
unsigned char img[HEIGHT][WIDTH][3]
のような配列に入っていましたがこれは
縦横のサイズがHEIGHTxWIDTH(480×640)の画像が

3つの種類の値(RBG値)を持つってことなんですね。

ちなみにunsigend char型は0〜255の値を持てるのでピッタリですね。
色ベクトルによる色認識
ここが今回のメインですね。
タイトルでは認識としていますが、実際は近い色を計算する、って感じです。
この近い色っていうのもあらかじめ候補に何色か設定しておいて、その中で何色に近いかを考えていく感じです。
色空間と色ベクトル
まずRGBを各軸に割り当てる直交座標系(色空間)というものを考えます。
以下のイメージです。

よくわるXYZ空間のようなものですね。
例えばある画素でのRGBの値が(100,50,200)だったとします、
その画素は色空間では以下のような場所に位置します。

この時に、原点からその画素を示す点に向けて伸びるベクトルを、その画素の色ベクトルと呼ぶことにします。

類似度
ここまでが前提です。そしてここから本題。
色の認識ですが、先ほど言った通りどの色に似ているか?を考えることで認識ということにします。
つまりあらかじめ色の候補を決めておいて、その中で一番似ている色がその画素の色という認識ってことですね。
あらかじめの候補の色ですが、ここはシンプルでわかりやすいので赤、青、緑とします。
それぞれRGB値は(255,0,0)、(0,255,0)、(0,0,255)です。
つまり上の画像の色空間では各軸を表していますね。

なのでここからは、
ある画素の色ベクトルが、どの軸に一番類似度が高いかを調べる問題になります
色ベクトルと、各軸のなす角を求める
類似度ですが、色ベクトルと各軸がなす角度を用いて判定していきます。
なす角度が小さい方が類似度がでかいってことです。(その軸に近いってことですね)
で、なす角度をどういやって調べんの?って話ですが
cosを計算することで解決します

cosは1に近い方が角度が小さいのでそれを利用します。
つまり角度そのものは計算しないけど、どの軸とのなす角が一番小さいかのみを調べるってことですね。
これまでの話をまとめると、
ある画素の色ベクトルを考えて、
そのベクトルと各軸のなす角度を用いたcosをそれぞれ計算する。
その3つのcosのうち最も1に近い軸が、
赤青緑のうち、その画素に最も近い色である。
って感じですかね。
この流れをコードにしていきます!
コードの実装
画像の読み書き
// カラー画像を読み込む
void Read_Color( const char *name, unsigned char img[HEIGHT][WIDTH][3] )
{
cv::Mat readImage( HEIGHT, WIDTH, CV_8UC3);
readImage = cv::imread(name, 1);
for( int w=0; w<WIDTH; w++ )
for( int h=0; h<HEIGHT; h++ )
for( int c=0; c<3; c++ )
img[h][w][c] = readImage.data[(w+h*WIDTH)*3+c];
}
// カラー画像をファイルへ保存
void Write_Color( const char *fname, unsigned char img[HEIGHT][WIDTH][3] )
{
cv::Mat IMAGE( HEIGHT, WIDTH, CV_8UC3 );
for( int w=0; w<WIDTH; w++ )
for( int h=0; h<HEIGHT; h++ )
for( int c=0; c<3; c++ )
IMAGE.data[(w+h*WIDTH)*3+c] = img[h][w][c];
cv::imwrite( fname , IMAGE);
}
こちらの関数で不明な点があればこちらの記事を参考にしてください!
cosを計算する関数
double color_cos(int a[3], int b[3]){
double abs_a = sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]);
double abs_b = sqrt(b[0]*b[0]+b[1]*b[1]+b[2]*b[2]);
double c = a[0]*b[0]+a[1]*b[1]+a[2]*b[2];
return c/(abs_a*abs_b);
}
これは仮引数で受け取るa、bのcosを計算する関数です。
a、bが3次元ベクトルで、上の計算式の通りにcosを計算してその値を返してます。
下で紹介する関数では、この関数に軸(R,G,B)のベクトルとある画素の色ベクトルを渡してそのcosを計算する感じですね。
類似度を求める関数
int color_sim(unsigned char img[HEIGHT][WIDTH][3], int x, int y){
int R[3] = {0,0,255};
int G[3] = {0,255,0};
int B[3] = {255,0,0};
int C[3];
for(int i=0; i<3; i++)
C[i] = img[y][x][i];
double cos_r = color_cos(R, C);
double cos_g = color_cos(G, C);
double cos_b = color_cos(B, C);
double sim = 1 - cos_r*cos_r;
int flag = 2;
if((1 - cos_g*cos_g) < sim){
sim = 1 - cos_g*cos_g;
flag = 1;
}
if((1 - cos_b*cos_b) < sim){
sim = 1 - cos_b*cos_b;
flag = 0;
}
return flag;
}
これは仮引数で受け取った配列imgに格納されているカラー画像の、座標(x,y)の画素の色を調べる関数です。
各ベクトルを定義
まず最初の部分
int R[3] = {0,0,255};
int G[3] = {0,255,0};
int B[3] = {255,0,0};
int C[3];
for(int i=0; i<3; i++)
C[i] = img[y][x][i];
ここでは各ベクトルを定義してます。
ややこしいんですが、先ほどまでRGBと連呼していたものの
openCVではBGRの順番で色情報が格納されます。
つまりimg[][][0]が青の画素値、img[][][1]が緑の画素値、img[][][2]が赤の画素値ってことです。
それに気をつけて定義しましょう。
Rは赤の軸なので{0,0,255}としています。(GBも同様)
Cは今回調べる画素の色ベクトルですね、img[x][y]の画素情報を抜き出してます。
最も1に近いcosを探索
double cos_r = color_cos(R, C);
double cos_g = color_cos(G, C);
double cos_b = color_cos(B, C);
double sim = 1 - cos_r;
int flag = 2;
if((1 - cos_g) < sim){
sim = 1 - cos_g;
flag = 1;
}
if((1 - cos_b) < sim){
sim = 1 - cos_b;
flag = 0;
}
return flag;
とりあえず全ての軸の分cosを計算します。
そのあと各cosに対して1-cosを計算して、最もその値が小さいものを探索します。
この値が小さいってことはより1に近いってことですからね。
そし1-cosが最小の軸に対応した変数flagを返すようにしてます。
赤なら2、緑なら1、青なら0です。
この数字を見てその画素が何色に類似しているかわかるってことですね。
ここまでで、色判別のコードはお終いです!
以下はこの関数を用いた応用です。
前回の複数物体の重心計算のプログラムに応用させる
void CoG2(unsigned char img[HEIGHT][WIDTH][3], int f[HEIGHT][WIDTH], int i){
double x_sum=0;
double y_sum=0;
double sum=0;
for(int h=0; h<HEIGHT; h++){
for(int w=0; w<WIDTH; w++){
if(f[h][w]==i){
sum ++;
x_sum += w;
y_sum += h;
}
}
}
int x = (int)x_sum/sum;
int y = (int)y_sum/sum;
int color = color_sim(img, x, y);
for(int i=-4; i<=4; i++){
for(int j=-4; j<=4; j++){
img[(int)y+i][(int)x+j][0] = 0;
img[(int)y+i][(int)x+j][1] = 0;
img[(int)y+i][(int)x+j][2] = 0;
}
}
for(int i=-2; i<=2; i++){
for(int j=-2; j<=2; j++){
img[(int)y+i][(int)x+j][color] = 255;
printf("%d\n",color);
}
}
}
これは見出し通り複数物体の重心を計算するプログラムなのですが、それに上で書いた関数を盛り込んでアレンジしたものです。
この重心を計算するコードに関してはこちらの記事で解説しているのでぜひ参考にしてください
アレンジの内容ですが、計算した重心の位置の画素の色を判定して、判定した色を重心の位置に印としてつけて返すって感じです。
ここの部分ですね
int color = color_sim(img, x, y);
for(int i=-4; i<=4; i++){
for(int j=-4; j<=4; j++){
img[(int)y+i][(int)x+j][0] = 0;
img[(int)y+i][(int)x+j][1] = 0;
img[(int)y+i][(int)x+j][2] = 0;
}
}
for(int i=-2; i<=2; i++){
for(int j=-2; j<=2; j++){
img[(int)y+i][(int)x+j][color] = 255;
printf("%d\n",color);
}
}
わかりやすいように黒の正方形を作ってからその上に印をつける感じです。
main関数
int main(void){
unsigned char img[HEIGHT][WIDTH][3]={0};
Read_Color("test2.png", img);
unsigned char bin[HEIGHT][WIDTH] = {0};
binary(img,bin);
int label[HEIGHT][WIDTH] = {0};
labeling(bin, label);
int i=1;
int flag = 0;
while(i<=k){
flag = 0;
for(int w=1; w<WIDTH-1; w++){
for(int h=1; h<HEIGHT-1; h++){
if(label[h][w]==i){
CoG2(img, label, i);
i++;
flag = 1;
}
}
}
if(flag==0)
i++;
}
Write_Color("bbb2.png", img);
Write("ccc2.png",bin);
}
ここも前回の記事と同じですので割愛
全体のコード
#include <iostream>
#include <opencv4/opencv2/opencv.hpp>
#define WIDTH 640
#define HEIGHT 480
#define t 200
// カラー画像を読み込む
void Read_Color( const char *name, unsigned char img[HEIGHT][WIDTH][3] )
{
cv::Mat readImage( HEIGHT, WIDTH, CV_8UC3);
readImage = cv::imread(name, 1);
for( int w=0; w<WIDTH; w++ )
for( int h=0; h<HEIGHT; h++ )
for( int c=0; c<3; c++ )
img[h][w][c] = readImage.data[(w+h*WIDTH)*3+c];
}
// カラー画像をファイルへ保存
void Write_Color( const char *fname, unsigned char img[HEIGHT][WIDTH][3] )
{
cv::Mat IMAGE( HEIGHT, WIDTH, CV_8UC3 );
for( int w=0; w<WIDTH; w++ )
for( int h=0; h<HEIGHT; h++ )
for( int c=0; c<3; c++ )
IMAGE.data[(w+h*WIDTH)*3+c] = img[h][w][c];
cv::imwrite( fname , IMAGE);
}
void binary(unsigned char org[HEIGHT][WIDTH][3], unsigned char byn[HEIGHT][WIDTH]){
for(int w=0; w<WIDTH; w++){
for(int h=0; h<HEIGHT; h++){
int sum=0;
for(int c=0; c<3; c++)
sum += org[h][w][c];
if(sum/3 > t)
byn[h][w] = 255;
else
byn[h][w] = 0;
}
}
}
int k=1;
void labeling(unsigned char byn[HEIGHT][WIDTH], int label[HEIGHT][WIDTH]){
int flag=0;
do{
flag = 0;
for(int w=1; w<WIDTH-1; w++){
for(int h=1; h<HEIGHT-1; h++){
if(byn[h][w]==0){
int sum=0;
int min = k;
for(int i=-1; i<=1; i++){
for(int j=-1; j<=1; j++){
sum += label[h+i][w+j];
if(0<label[h+i][w+j]&&label[h+i][w+j]<=min)
min = label[h+i][w+j];
}
}
if(sum==0){
label[h][w] = k++;
flag++;
}else if(min == label[h][w])
;
else{
label[h][w] = min;
flag++;
}
}
}
}
}while(flag > 0);
}
double color_cos(int a[3], int b[3]){
double abs_a = sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]);
double abs_b = sqrt(b[0]*b[0]+b[1]*b[1]+b[2]*b[2]);
double c = a[0]*b[0]+a[1]*b[1]+a[2]*b[2];
return c/(abs_a*abs_b);
}
int color_sim(unsigned char img[HEIGHT][WIDTH][3], int x, int y){
int R[3] = {0,0,255};
int G[3] = {0,255,0};
int B[3] = {255,0,0};
int C[3];
for(int i=0; i<3; i++)
C[i] = img[y][x][i];
double cos_r = color_cos(R, C);
double cos_g = color_cos(G, C);
double cos_b = color_cos(B, C);
double sim = 1 - cos_r*cos_r;
int flag = 2;
if((1 - cos_g*cos_g) < sim){
sim = 1 - cos_g*cos_g;
flag = 1;
}
if((1 - cos_b*cos_b) < sim){
sim = 1 - cos_b*cos_b;
flag = 0;
}
return flag;
}
void CoG2(unsigned char img[HEIGHT][WIDTH][3], int f[HEIGHT][WIDTH], int i){
double x_sum=0;
double y_sum=0;
double sum=0;
for(int h=0; h<HEIGHT; h++){
for(int w=0; w<WIDTH; w++){
if(f[h][w]==i){
sum ++;
x_sum += w;
y_sum += h;
}
}
}
int x = (int)x_sum/sum;
int y = (int)y_sum/sum;
int color = color_sim(img, x, y);
for(int i=-4; i<=4; i++){
for(int j=-4; j<=4; j++){
img[(int)y+i][(int)x+j][0] = 0;
img[(int)y+i][(int)x+j][1] = 0;
img[(int)y+i][(int)x+j][2] = 0;
}
}
for(int i=-2; i<=2; i++){
for(int j=-2; j<=2; j++){
img[(int)y+i][(int)x+j][color] = 255;
printf("%d\n",color);
}
}
}
int main(void){
unsigned char img[HEIGHT][WIDTH][3]={0};
Read_Color("test2.png", img);
unsigned char bin[HEIGHT][WIDTH] = {0};
binary(img,bin);
int label[HEIGHT][WIDTH] = {0};
labeling(bin, label);
int i=1;
int flag = 0;
while(i<=k){
flag = 0;
for(int w=0; w<WIDTH; w++){
for(int h=0; h<HEIGHT; h++){
if(label[h][w]==i){
printf("%d \n",i);
CoG2(img, label, i);
i++;
flag = 1;
}
}
}
if(flag==0)
i++;
}
Write_Color("bbb2.png", img);
Write("ccc2.png",bin);
}
実際に使ってみた
この画像を以上のプログラムにかけてみます。

結果が、、

これはいけてますね。
ただ簡単すぎる画像なのでもう一枚試してみましょう

結果は、、、

おお
予想通りの色に認識できましたね。
まとめ
てことで動作確認もできたのでこの記事は終了です!!
今回はシンプルかつ分かりやすくするために、色空間上の軸(赤、青、緑)の3色のみで識別しましたが、それのみに限らず他の色のベクトルもあらかじめ用意してcosを計算すれば同様にその色に近いかを判定できます!
ぜひコードを改造してやってみてください!!
てことでここまでです。
ではまた〜
コメント