【C++】一次差分法による画像のエッジ検出プログラム【画像処理の基本】

スポンサーリンク
一次差分法検証結果 画像処理
スポンサーリンク

はじめに

今回はエッジ(画像中の対象と背景との境界のこと)の検出を行なっていきます。

画像の種類や動作条件については、こちらで二値化を行った時と同様です。

プログラムにするのは簡単なので具体的なアルゴリズムについては省略します。

一次差分法について

エッジ検出における一次差分法は、ある画素から見て横方向と縦方向に一つずつ輝度の差分をとり、その合計値を輝度とする手法です。

具体的には次のようになります。

ある特定の画素\((x,y)\)の輝度値を\(E_{x,y}\)とし、一次差分法適応後の輝度値を\(E_{x’,y’}\)とすると

\[E_{x’,y’} = \left|2E_{x,y} – \left(E_{x+1,y} + E_{x,y+1}\right)\right|\]

プログラムでは画像を読み込んでから、全ての画素にこの式を適用します。

ただし、画像の端にあたる部分については処理を書くのが少し手間だったので除外します。

ソースコード

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
#include <cmath>

std::string input = "input.pgm";

int main(){

	std::ifstream ifs(input); //入力ファイルストリーム
	std::string str_i; //文字列型
	
	if(ifs.fail()){ //ファイルを開けない時のエラー処理
	
		std::cerr << "Failed to open file." << std::endl;
		
		return -1;
	}
	
	getline(ifs, str_i); //一行目の読み込み
	
	if(str_i != "P2"){ //P2画像でない時のエラー処理
		
		std::cerr << "Can not handle." << std::endl;
		
		return -1;
	}
	
	int width,height;
	
	while(1){ //コメント行から最大値行までを読み込み
		
		getline(ifs, str_i); //一行を読み取り
						
		if(str_i[0] != '#'){ //コメント行でなければ最大値行を読み込んでループ処理を抜ける
			
			int sp_p = str_i.find_first_of(' ');
			
			std::string wid_str = str_i.substr(0,sp_p);
			std::string hei_str = str_i.substr(sp_p+1);
			
			width = atoi(wid_str.c_str());
			height = atoi(hei_str.c_str());
					
			getline(ifs, str_i); //最大値の読み込み
			
			int max = atoi(str_i.c_str());
	
			if(max > 255){ //最大値が255より大きい時のエラー処理
				
				std::cerr << "Can not handle." << std::endl;
				return -1;
			}
			
			break;
		}
	}
	
	int data[width][height];
	int row = 0,column = 0;
	
	int st_value[3] = {-1,-1,-1}; //一時記憶用の配列
	
	while(getline(ifs, str_i)){ //最終行まで繰り返し,一行ずつ読み込み
		
		for(int i = 0;i < str_i.length();i++){ //文字列の長さだけループ
		
			if(str_i[i] == '#'){ //コメントがついた時点でループを抜け出す.
				
				break;
			}
			else if(str_i[i] == ' '){ //空白だった時の処理
				
				int digits = 0;
				int value = 0;
				
				for(int j = 2;j >= 0;j--){ //配列にスタックされたデータを全て取り出す.
					
					if(st_value[j] > -1){
						
						value += st_value[j]*std::pow(10,digits); //各桁の数字から実際の数値に変換(ab) > (10*a + b)
						digits++;
					}
				}
				
				if(value > 0 || st_value[0] == 0){ //数値が何かあった場合の処理
					
					if(row == width){
						
						column++;
						row = 0;
					}
					data[row][column] = value;
					row++;
				}
				
				for(int l = 0;l < 3;l++){ //配列の初期化
					
					st_value[l] = -1;
				}
			}else{ //空白でなかった時の処理(データを格納する場所を探す)
				
				for(int k = 0;k < 3;k++){ //0から2までで格納(最大値が255)[0] = 一の位 [2] = 100の位
					
					if(st_value[k] == -1){ //-1であれば値が入っていない=まだ決まっていない桁
					
						int data_v = 0;
						
						data_v = (int)(str_i[i] - '0'); //char型の数字をint型に変換
						st_value[k] = data_v; //各桁の数字のみを配列に格納
						
						break;
					}
				}
			}
		}
	}
	
	//開いたファイルを閉じる
	ifs.close();
	
	std::cout << "一次差分法を適応します." << std::endl; 
				
	int f_data[width][height];
	
	for(int j = 0;j < height-1;j++){
		for(int i = 0;i < width-1;i++){
			
			f_data[i][j] = 255 - abs(2*data[i][j] - data[i+1][j] - data[i][j+1]); //差分の計算
		}
	}
	
	for(int i = 0;i < width;i++){ //外周を除去
			
		f_data[i][height-1] = 255;
	}
	
	for(int i = 0;i < height;i++){ //外周を除去
			
		f_data[width-1][i] = 255;
	}
	
	f_data[width-1][height-1] = 255; //角を除去
	
	std::ofstream offs("xyf_differ.pgm"); //出力ファイルストリーム
	
	if(offs.fail()){ //ファイルを開けない時のエラー処理
	
		std::cerr << "Failed to open file." << std::endl;
		
		return -1;
	}
	
	offs << "P2" << std::endl; //P2(グレースケール画像)を指定 一行目の書き出し
	
	offs << width << ' ' << height << std::endl;
	
	offs << "255" << std::endl;
	
	for(int j = 0;j < height;j++){
		for(int i = 0;i < width;i++){
			
			offs << f_data[i][j] << std::endl; //書き出し
		}
	}
	
	offs.close();
		
	return 0;
}

プログラムを実行する

次の画像に対してプログラムを実行する。

結果として、次のような画像が出力された。

一次差分法検証結果

最後に

一次差分法ははっきりとした輪郭が存在するものであれば、ほぼ確実にエッジを検出しますが、雑音を捉えやすいかと思われます。

そのほかのエッジ検出法については別の記事で書いていきます。

今回はこの辺で。

コメント

タイトルとURLをコピーしました