C言語 構造体
6章 構造体
構造体とは、操作しやすいように一つの名前でまとめられた
1つ以上の異なった型の集まりである
この構造体と呼ばれるものは大きなプログラムで特に複雑なデータを組織化するのに役立つ
それは関連したデータを実体としてでなく一つの単位として扱えるという性質から来ている
6.1 構造体の基本事項
まずは構造体を使って二次元平面上の点を構造体を使って表してみる
int x,y;
普通はこう表すかもしれないけど、構造体でひとつの単位としてまとめるとこうなる
struct point{ int x; int y; };
これでpointという平面上での座標を表す一つの単位が出来た
前者に比べて後者のほうが何を示しているかわかりやすくなる
ここで少し説明するとstructという予約語の後に続いている名前は構造体タグと言われるものである
この構造体タグなるものは宣言以後、カッコ内の宣言部分の略称としてしようすることができる
構造体の中で命名された変数はメンバーと言われる
そして構造体タグとメンバー名は同名であってもぶつかることはない
構造体を使いたいときは以下のような記述をする
struct point pt;
これによりptはstruct point型であると宣言される
また以下のように各メンバーに対する定数式による初期化式の並びを定義の後につけることで構造体を初期化することができる
struct point max_pt = {100,100};
さらに宣言だけしておいて以下のように別々に初期化することも出来る
struct point min_pt; min_pt.x = -100; min_pt.y = -100;
上の例でわかるように
構造体型の変数名.メンバー名 >|| でその構造体のメンバにアクセスできるため、値を代入するときだけじゃなくて 値を呼び出したいときにも使用することが出来る 構造体は入れ子にすることも出来て 以下のようなメンバーを持つ構造体を宣言することも出来る >|c| struct rect{ struct point pt1; struct point pt2; };
この構造体は2つのpoint構造体をメンバとして含んでいる
ここでscreenを以下のように宣言しすると
struct rect screen
これでxが呼び出せる
screen.pt1.x
ここで紹介した以外にも構造体は以下のようにも宣言できる
struct{}x,y;
実は構造体タグは省略可能
6.2 構造体と関数
構造体に対して許される唯一の演算はコピーすること
&でそのアドレスを求めること
コピーと代入には関数に渡して関数から値を返すことも含まれている
しかし構造体の比較はできない
ここでは関数の値として構造体を返すことを例にあげてみる
struct point makePoint(int x,int y){ struct point tmp; tmp.x = x; tmp.y = y; return tmp; }
この関数はstruct point型の構造体を返すもので、前に書いたようにメンバー名と引数名が衝突していないことにも注目したい
ここで作成したmakePoint関数は任意の構造体を動的に初期化することができる
次に示すのは関数の引数と戻り値が構造体の場合のとき
struct point addPoint(struct point pt1,struct point pt2){ pt1.x += pt2.x; pt1.y += pt2.y; return pt1; }
ここは明示的な一時変数を使わないでpt1のメンバに加算しているが
これは構造体のパラメータが他のものと同じように値によって渡されることを強調するためである
これまでは構造体をそのまま渡してきたが関数に対して大きな構造体を渡す場合は構造体全体をコピーするより
ポインタを渡すほうが一般に能率的である
構造体ポインタは普通の変数へのポインタと変わらず以下のように宣言できる
struct point *pp;
これは変数ppがstruct point型へのポインタであることを示す
この時構造体ポインタのメンバにアクセスする場合は以下のようにする
(*pp).x; (*pp).y;
この時注意すべきことは*は.より優先順位が低いため()が必要になる
これを用いて以下のようなコードが書ける
struct point base; struct point *pointer; pointer = &base; printf("%d,%d",(*pointer).x,(*pointer).y);
また構造体のポインタへの参照はこれ以外にも別の方法がある
上の例に続いて、pointerが構造体へのポインタだとすると
pointer -> メンバー
のようにすることでメンバーを参照出来る
そこで2つ上の例は以下のように出力処理を書き直せる
printf("%d %d",pointer->x,pointer->y);
6.3 構造体配列
構造体自体は配列にすることができる
ここまでのまとめに準ずるなら
#define NUM 10 struct point pt[NUM];
もしくは
#define NUM 10 struct point{ int x; int y; }pt[NUM];
更にいうのであれば
struct point{ int x; int y; }pt[] = { 1,1, 2,1,... };
のようにも定義出来る
この時構造体は配列になっているので、構造体のメンバーにアクセスしたい場合は
//iを0以上の任意の整数として
point[i].x
point[i].y
見たいな風にすれば、その構造体のインデックスに対応したメンバーを参照することが出来る
6.4 typedef
ここで少し話は逸れるがtypedefという新しいデータ型を宣言するための機能を紹介する
ただこの説明ではわからないと思うので実際に例を上げる
typedef int Length;
これでLengthをint型と同意義のものとして定義する
なので以下のような構文が書ける
Length len = 10; Length maxLen = 20;
ということは文字列関連でいつもめんどくさいなと思っていたことを解消できる
それは文字列である。文字列はC言語においてポインタや配列で実現するのが通例になっているが
JavaやC++,C#といった言語を先に経験した人にとってはこれは煩わしいものであったと思う
それをtypedefを使って以下のようにすれば
typedef char *String;
他の言語と同じように文字列を宣言できる
String str = "hello,world!!";
ちなみに同意義のものとして新しい型を既存の型と結びつけるので
typedefで宣言した新しい型でキャストすることもできる
例えば
String str;
str = (String)malloc(100);
のように
6.5 共用体
共用体(union)と言われるものは構造体とすごく似ているが、異なる型を同一のメモリで扱うことが出来る
というのも共用体のすべてのメンバにおいてオフセットが0である構造体のようなものだからである
共用体のメンバは構造体と同じように参照できる
ただ共用体自体の宣言が少し違う
union SampleUnion{
};
これでひと通りの構造体、それに類似するもののまとめは終わり
C言語 ポインタと配列
5章 ポインタと配列
ポインタは、他の変数のアドレスを格納する変数でありC言語では頻繁に利用される
理由は大きく分けて2つある
一つ目としてポインタを使うことが処理を行う唯一の方法である場合があるから
二つ目はポインタを用いて記述したほうが、それを使用しないで記述されたコードよりコンパクトで効率的になる場合があるから
そしてポインタと配列には密接な関係がある
しかしポインタは理解不能なプログラムをつくってしまうものとしてgotoとどっこいの扱いを受けてきた
確かに不用意にポインタを使うと理解不能なプログラムになってしまうが十分に注意することでポインタを活用し
よりよいコードを書くことも出来る。これはさっき述べた理由の二番目に合致する
ANSI Cではポインタの扱いが明確に定義され型void *(voidへのポインタ)をchar *の代わりに一般化されたポインタとして利用出来るようになった
5.1 ポインタとアドレス
これはポインタを扱う上で基礎的な知識になる
まずメモリ構成について、普通コンピュータでは個々にあるいは連続したグループとして扱うことの出来る
連続番号付きのつまりはアドレス付きのメモリセルという形の配列を持つ、これは配列とポインタの関係を説明する上で重要な点になる
そして、ポインタとはアドレスを格納することができるので
以下のように書ける
int *p; int n = 1; p = &n;
まず一行目でint型のポインタを宣言し四行目でint型変数のアドレスを代入している
ここで使用した&演算子はオブジェクトのアドレスを示すものであり、変数か配列の要素にのみ使用できる
逆に式や定数、レジスタ変数には適用できない
次にポインタを示す*演算子は間接演算子、逆参照演算子と言われる
これをポインタに適用にするとそのポインタが示すオブジェクトにアクセスできる
具体的には以下のように使える
int *p; int x = 1, y = 2; p = &x;//変数xのアドレスを代入 y = *p;//アドレスが示すオブジェクトつまり1を代入 printf("%d\n",y);//print -> 1
また以下のような文があったとしよう
double *dp,atof(char *);
この場合*dpとatof(char *)がdouble型をもちatofの引数charへのポインタであることを示すと同時に
ポインタがある特定の種類のオブジェクト指すように制約されている
そのため各ポインタは特定のデータ型を指す
ただし例外がありvoidへのポインタは任意の型のポインタを保持できるがそれ自身を逆参照することは出来ない
それではさっきの例に戻って*pがxを表すなら*pはxの代わりにどの文脈でも使用できる
つまり
//*p,x=1,p=&xを仮定 *p+=2;//これでxとpは3を表す *p = *p + 2; ++*ip; //これだけ演算子の関係でかっこが必要 (*p)++;
このように自由に計算できるが*pはxのアドレス、つまりxが存在する場所を参照できるので演算結果がxにも影響する
さらに最後の場合で単行演算子は右から左へ評価されるためipだけがインクリメントされてしまうなど注意が必要
さらに*qを同じくintへのポインタだとすると
q = p;
とすることでpがqにコピーされqはpを示すものとなる
5.2 ポインタと関数引数
以下のような関数を考える
void swap(int x,int y){ int tmp = 0; tmp = x; x = y; y = tmp; }
これは何ら問題なくコンパイルできるし一見正しく動作するように思えるが
関数内部の処理をどう変更してもこのままでは外部に全く影響しないのでswapしてるようでswapしてない
一種のバグを生み出している
それもそのはず、関数の引数はポインタやアドレスが関係しない限り渡した値のコピーでしかないからだ
ただポインタを使えば、アドレスを参照することができるので変更が外部にも適用される
なので以下のように変更すると想定どおりの動作をするだろう
void swap(int *x,int *y){ int tmp = 0; tmp = *x; *x = *y; *y = tmp; }
このようにポインタを使うと簡単に実装できたりポインタがなければ実装できない処理もある
またわかりやすい例を上げるならscanf関数である、scanf関数は入力を格納したい変数のアドレスを渡しているため
scanf関数外にある変数に値を代入できる
5.3 ポインタと配列
ポインタと配列には強い関係がある
というのも配列でインデックスを指定して実行出来る操作がポインタでも出来るためである
ポインタを使った操作のほうが一般に高速だとされているが初心者には理解しにくい
まず例を上げる
int a[10];
という宣言があるとき、配列はa[0],a[1],a[2]...と連続するオブジェクトからなるブロックである
a[i]といった記法は先頭からi番目の要素を参照する
また以下のintへのポインタ
int *p;
が宣言されているとすると、以下の代入によって
p = &a[0];
aの0番目の要素のアドレスがpにセットされる
さらにintへのポインタpに対して配列aの特定の要素が定義されている場合
p+1で次の要素を指す
一般にp+iはi番目の要素を指す
したがって、pがa[0]を指しているなら
*(p+1)
はa[1]の要素を参照する
またp+iはa[i]のアドレスであり*(p+i)はその要素を指す
ここまでで説明したものは配列aの変数の型やサイズに関わらず当てはまる
インデックシングとポインタ演算の間の対応は非常に良く、定義により配列型の変数あるいは式の値は配列の先頭の要素のアドレスであるから
p = &a[0];
のあとでpとaは同じ値を持つ。つまり配列の名前はその先頭の要素の位置と同義であるから
以下のようにも書ける
p = a;
この前にもさらっと書いたがa[i]への参照は*(a+i)とかけることからそれもまた同等であると言える
またその等式に&演算子を適用すると&a[i]とa+iは同じということになる
ただしここで一つだけ注意しないといけないことがある
p = a;
でおこなわれた定義に対して
p++;
は意味のある演算であるが
a = p; a++;
のような演算は正しくない
配列名が関数などに渡されるとき渡されるのは配列の先頭のアドレスであって
呼び出された関数内では局所的な変数として扱われる
したがって配列名のパラメータはポインタはである
これらを使って文字列の長さを求めるstrlen関数を新しく作ってみる
int strlen(char *str){ int lencount; for(lencount = 0; *str != '\0'; str++,lencount++); return lencount; }
このコードにおいてstr++というのはstrがポインタなので許される
それらは関数内の文字列に何ら影響しない
これはポインタの単にstrlen内でのプライベートなコピーを演算しているだけに過ぎない
ここまでまとめたことにより
char str[]
と
char *str
は関数の仮引数として同一であると言える
そして以下のように配列をポインタに渡した場合
//int *p;int a[n];を仮定
p = a;
以下のような操作も出来る
p[i]
5.4 アドレス演算
今pが配列のどれかの要素へのポインタだと仮定すると、p++でインクリメントされると次の要素を指すようになる
同様にp+=iならi要素分だけ先を指すようになる
このような形はポインタ、アドレス演算の典型的な形である
Cではアドレス演算のアプローチが首尾一貫してて規則的になっている
ポインタ、配列、アドレス計算を統合したのはこの言語の主な強力さの一部になっている
まずmallocなどの関数を使ってメモリをメモリを確保し
使い終わったらfree関数などを使って確保したメモリを解放する2つのルーチンがある
これらはいままとめた順序で行う必要があり
順番が違っても、どちらかが抜けてもエラーやバグの原因となるので注意
そして特に配列が絡む場合ポインタは+,-などの演算子を使って計算することができる
というのも結果的にどの要素を指しているかということなので==,<=,!=,<,>,>=といったものも使える
特に2つの配列のポインタp,qを仮定したとき
p < q
などとするとpとqの場所が比較される
また演算子で計算できることから
さっき書いたstrlen関数を以下のように書きなおすこともできる
int strlen(char *str){ char *p = str; while(*p != '\n') p++; return p - s; }
5.5 文字ポインタと関数
以下のように記述された文字列定数は文字の配列である
"hello,world"
別の章でも言ったように実際はこれの末尾にnull文字があるので文字列のサイズは
ダブルクォートの中身の文字の数より1多い数値になる
おそらく最も文字列定数が現れるのは以下のような関数への引数として現れるときだろう
printf("hello,world");
このようなものが実際にコードの中にあるとき、それへのアクセスは文字ポインタを通して行われる
ただし実際にprintf関数が受け取るのは文字列定数の初めのアドレスである
話は変わって、ここで文字列定数が以下ように
char *pmsg = "hello,world";
や
char amsg[] = "hello,world";
と宣言、定義された場合
pmsgとamsgでは明確な違いがある
この単元の最初でも言ったように文字列定数は配列である
それを配列に代入した場合、文字数とnull文字を含めたサイズ分だけの大きさを持つ
この配列の中身の文字は変わるかもしれないが、amsgは常に同じアドレスをさす
一方pmsgのようにポインタを使った場合、pmsgには文字列のポインタのみが代入される
したがってこのポインタをあとで別の場所を参照するように変更することもできる
しかし文字列の内容を変えようとするとその結果は不定となるので注意が必要
5.6 ポインタのポインタ
ポインタとはそれ自身がアドレスを格納する特殊な変数なので
それ自身も変数のアドレスをメモリのどこかに確保していることになる
なのでポインタ自身もアドレスをもつということがわかる
C言語ではポインタのアドレスを更にポインタに格納することが可能である
例えばintへのポインタのポインタを作るなら
int **p;
のように宣言できる
ポインタの連鎖自体は何個でも作ることができるが自分が管理できる範囲にしておくのがいい
またこの考えはポインタ配列というものにも適用できて以下のような関数が書ける
複数個の文字列の配列に対して
void swap(char *str[],int i,int j){ char *tmp; tmp = str[i]; str[i] = str[j]; str[j] = tmp; }
このようなことも出来る
Javaオブジェクトのライフサイクル管理
はじめに
javaではnewを使うことで簡単にオブジェクトを生成できるが
プログラムの規模が大きくなると、オブジェクトの生成から消滅までの管理が複雑化してしまう
オブジェクトのライフサイクルを適切に管理するには以下のようなやり方がある
・変数のスコープに注意して不要に長い寿命のオブジェクトを減らす
・寿命の長いオブジェクトと短いオブジェクトを分離する
・ファクトリパターンなどを駆使する
・いっそフレームワークなどに隠す
このなかで一番簡単なファクトリパターンと
クラス自体を利用する方法についてまとめていく
ファクトリパターンとは
ファクトリパターンとはオブジェクト生成を一つの役割とみなしてそれを分離してしまうこと
またこれにはいくつかの技法があるが今回はファクトリメソッドに隠してしまいます
このときファクトリメソッドはstaticである必要があります
またファクトリメソッドを使うとするなら、コンストラクタは外部から参照出来ないようにprivateにしておくとよい
これはファクトリメソッドを使用する以外の方法でオブジェクトが生成されるのを防ぐため
実際のコード
これらを踏まえてファクトリパターンを使ったコードを実際に書いてみる
public class Test{ //コンストラクタを外部から参照できないようにする private Test(){ } //ファクトリメソッド static Test getInstance(){ /* * Test test = new Test(); * return test;でもいい */ return new Test(); } }
OpenGLをC言語で扱う①
空ウィンドウを表示する
まずはとりあえずウィンドウだけを表示してみる
これ自体はすごく簡単なので先にサンプルコードを示す
#include<stdio.h> #include<GL/glut.h> void display(void); int main(int argc,char *argv[]){ //call window glutInit(&argc,argv); glutCreateWindow("sample window");//string literal or argv(string) glutDisplayFunc(display); glutMainLoop(); return 0; } void display(){ //pass }
まずここで登場する関数について説明
関数名 | 機能 |
---|---|
void glutInit(int *argcp, char **argv) | OpenGLの初期化を行いmain関数のパラメータをそのまま渡す |
int glutCreateWindow(char *name) | ウィンドウを開きタイトルバーの設定などを行う |
void glutDisplayFunc(void (*func)(void)) | ウィンドウ内に描画する関数。描画内容は関数のポインタとして渡した関数に依存 |
void glutMainLoop(void) | イベントを待ち受ける関数 |
ざっくりまとめるなら
初期化→ウィンドウ作成→描画内容固めた関数渡して→ループさせる
みたいな感じで空ウィンドウは作成出来る
ただ一つ描画内容を固めるdisplayという独自関数がいつ呼ばれるのか
メインループがglutCreateWindowでウィンドウの生成が完了した段階で呼び出される
さらに言うなら再描画が必要なときも呼び出される
ウィンドウを塗りつぶしてみる
空ウィンドウのプログラムを実行するとわかるがウィンドウ下がそのまま表示されてしまっている
しかも位置は最初に呼び出した場所で固定
これではあまりに微妙な感じが出ているのでとりあえずウィンドウの背景を塗りつぶしてみる
#include<stdio.h> #include<GL/glut.h> void display(void); void init(void); int main(int argc,char *argv[]){ //OpenGLの初期化 glutInit(&argc,argv); //ディスプレイの表示モードの指定 glutInitDisplayMode(GLUT_RGBA); glutCreateWindow("sample window ver 2"); glutDisplayFunc(display); //描画で使うものの初期化 init(); glutMainLoop(); return 0; } void display(){ glClear(GL_COLOR_BUFFER_BIT); glFlush(); } void init(){ glClearColor(0.0,0.0,0.0,1.0); }
ここで初めて出てきた関数の名前と機能
関数名 | 機能 |
---|---|
void glutInitDisplayMode(unsigned int mode) | ディスプレイの表示モードを指定する関数、GLUT_RGBAとGLUT_INDEXが使える |
void glClearColor(GLclampf R, GLclampf G, GLclampf B, GLclampf A) | ウィンドウを塗りつぶす色を指定する関数、α値は1で不透明となる |
void glClear(GLbitfield mask) | ウィンドウを塗りつぶす関数、塗りつぶすバッファを指定して使う |
glFlush(void) | まだ実行されてない命令をすべて実行させる関数。あまり呼び出しすぎると描画速度が低下する |
ここでは色の指定などそういった初期化をinit(void)にまとめている
main関数やdisplay関数にまとめてもいいんけど、今後描画するものが多くなった時、複雑になった時などに
そういう記述が邪魔になるので分割する
2次元図形の描画と色の変更
今回はとりあえず今回は正方形を描画してみるが、ここで注意しなければならない点は座標は[-1,1]の範囲で固定されているという点で
これはx軸y軸の両方に言えることで、簡単に言えば単位円がちょうど書ける大きさというわけである
#include<stdio.h> #include<GL/glut.h> void display(void); void init(void); int main(int argc,char *argv[]){ glutInit(&argc,argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow("square 2d"); glutDisplayFunc(display); init(); glutMainLoop(); return 0; } void display(void){ glClear(GL_COLOR_BUFFER_BIT); //描画 //描画タイプの指定 glBegin(GL_LINE_LOOP); //線の色を指定 glColor3d(0.0,0.0,1.0); //座標を設定 glVertex2d(0.8,0.8); glVertex2d(-0.8,0.8); glVertex2d(-0.8,-0.8); glVertex2d(0.8,-0.8); //設定終了 glEnd(); //図形だけじゃ見づらいのでx,y軸も追加 glBegin(GL_LINES); glColor3d(0.0,0.0,0.0); glVertex2d(-1,0); glVertex2d(1,0); glEnd(); glBegin(GL_LINES); glColor3d(0.0,0.0,0.0); glVertex2d(0,-1); glVertex2d(0,1); glEnd(); glFlush(); } void init(void){ //黒が背景は見づらいので白に変更 glClearColor(1.0,1.0,1.0,1.0); }
今回は表にするまでもなく手順が簡単なのでざっくり説明
glBeginで描画タイプ設定
↓
glColor3dで線の色を設定できるので必要に応じて設定
↓
glVertex2dで座標を設定x,yの順番
↓
glEndで設定終了、描画
glFlushで強制的に描画できる
また描画タイプを変更することで図形内部を塗りつぶしたりもできる
RaspberryPiにSambaをインストールしてサーバー化する
はじめに
結構前にRaspberryPI B+を買っていて
そのままになっていたのを発見したのでsambaでもインストールしてサーバー化しようと思う
sambaの設定
インストールすると/etc/samba/smb.confというsambaの設定ファイルが生成される
なのでこれを
sudo vim /etc/samba/smb.conf
とかやって開く
そうすると見ればわかるがsambaの設定は[]で区切られている
今回は全体の設定をいじるので[global]の設定を変更する
Samba の導入 - Linux で自宅サーバ [ Home Server Technical. ]
設定についてはこのサイトが詳しくのっているので丸投げ
sambaにユーザーを追加する
このままでは使用できないのでsambaにユーザーを追加する
今回はraspbianで操作しているのでまずraspbian側にユーザを追加する
もう存在するユーザを使うのであればここは無視していい
sudo adduser [USERNAME]
ここからraspbianに存在するユーザを追加する作業
sudo pdbedit -a [USERNAME]
後はそれをsambaに
sudo smbpasswd -a [USERNAME]
これで使えるようになる(はず)
追記(2016/02/28)
raspbianにインストールするとprintersとprint$っていう設定グループがデフォルトであるけど
これがあるとなかなか設定が変更しにくいので[printers]がある行から下を全部コメントアウトして
以下をその下に記述するとなかなか扱い易かった
[Share] path = [公開したいフォルダのパス] writable = yes guest ok = no guest only = no create mode = 0777 directory mode = 0777 share modes = yes
完全に利用をユーザ登録されている人に限定した設定になっているので
ゲストもokにしたいのなら
guest ok = yes
guest onlu = yes
に変更してください
移動平均法
移動平均法
今回は移動平均法を自分で実装してみた
そもそも移動平均法とは、一枚の画像に含まれるノイズを除去する基本的な手法の一つで
入力画像における(i,j)成分の濃度に対しその近傍の濃度値の平均値を出力画像の(i,j)成分とする方法のこと
その過程により画像の濃度のばらつきを低下させるため必然的にノイズも除去されるが
出力画像の輪郭などが鈍る傾向にある
具体的な処理
近傍の濃度値の平均は以下の式で求めることが出来る
ここでの[]はガウス記号
実際この通りにコードを書いていけばいいが
プログラミングで扱う以上リストや配列の範囲を超える場合があるので
その時のループはスキップすることで実装できる
コード
#coding:utf-8 import numpy as np import math import cv2 #ガウス記号が示すものを関数化したもの def gauss(number): if 0 <= number: return int(math.floor(number)) else: return int(math.ceil(number)) image = "lenna256.png" result = "lenna256_move.png" #今回は256次正方行列を使うため #256*256画素にリサイズしたlenna256.pngを使用 img = cv2.imread(image) #グレースケールに変換 gray = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) #移動平均法を行う for row in range(0,len(gray)): for line in range(0,len(gray[0])): #近傍濃度の和 s = 0 #3x3の正方形で計算する n = 3 for i in range(-1*gauss(n/2.0),gauss(n/2)+1): for j in range(-1*gauss(n/2.0),gauss(n/2)+1): if len(gray) <= i + row or len(gray[0]) <= j + line: continue s += gray[i+row][j+line] gray[row][line] = s/n**2 #cv2.imwrite(result,gray) cv2.imshow("move average",gray) cv2.waitKey(0) cv2.destroyAllWindow()
判別分析法
判別分析法
判別分析法とはざっくり言えば2値化の手法の一つで
分離度という値が最大になる値を見つけ、自動的に2値化する手法
分離度はクラス間分散とクラス内分散の比で求めることができる
具体的な仕組み
今、しきい値を
として
これがまず判別分析法を使う上でのデータの定義
そして次がクラス内分散とクラス間分散、全分散の定義
ここまでが判別分析法で扱う定義の部分
これら与えられた条件を元に分離度を算出する
そしてこれが最大になるしきい値tを求めることで2値化する
しかし全分散はしきい値に関係なく一定なのでクラス間分散の分子
が最大になるしきい値tを求めることになる
なので0~255の間で上の式が最大になるようなtを求めていけばいい
そしてそれらの条件を満たすtを見つけたら
それを元に2値化処理をすることで、判別分析法が実装できる
実装コード
#coding:utf-8 import numpy as np import cv2 img = cv2.imread("lenna256.png") #画像をグレースケールに変換 gray = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) #ヒストグラムを作成 histgram = [0]*256 for i in range(0,len(gray)): for j in range(0,len(gray[0])): histgram[gray[i][j]] += 1 #print histgram max_t = max_val = 0 #判別分析法を使って2値化 for t in range(0,256): #画素数 w1 = w2 = 0 #クラス別合計値 sum1 = sum2 = 0 #クラス別平均 m1 = m2 = 0.0 for i in range(0,t): w1 += histgram[i] sum1 += i*histgram[i] for j in range(t,256): w2 += histgram[j] sum2 += j*histgram[j] #0除算を防ぐ if w1 == 0 or w2 == 0: continue #クラス別平均の算出 m1 = sum1/w1 m2 = sum2/w2 #結果を算出 result = w1*w2*(m1-m2)*(m1-m2) if max_val < result: max_val = result max_t = t for i in range(0,len(gray)): for j in range(0,len(gray[0])): if(gray[i][j] < max_t): gray[i][j] = 0 else: gray[i][j] = 255 #print max_val #cv2.imwrite("binary_lenna.png",gray) cv2.imshow("binary",gray) cv2.waitKey(0) cv2.destroyAllWindow()
せっかくなので今回はヒストグラムも自作してみた