どうもー
今回は前回の続きでまた二値化を行っていきます!
前回は独立閾値決定法によって閾値を決めて二値化を行っていましたが、
今回は条件付き閾値決定法によって閾値を決めて二値化を行う方法を紹介したいと思います!
前提的なもの
前提として僕はXcodeでコードを書いて、実行しています。
opencvはhomebrewからダウンロードして、xcodeからパスをつないだり必要なファイルなどをブチ込んだり、、、とバンバンに使える状態から始めています。
Xcodeにopencvを導入する方法はgoogleで調べればいくらでもあるので、まだ導入できてない人はまずそこをお願いします。(気が向いたらこのサイトでもopencvの導入の記事を書くかも
条件付き閾値決定法とは
前回扱った独立閾値決定法では、対象の画素を白にするか黒にするかをその画素の濃度値のみで決定していました。
それに対して条件付き閾値決定法はその画素だけでなく、周辺の画素の濃度値も参照しながら閾値を決定します。
すなわち濃度値を持つ各画素を二値化するという操作をある範囲で行うことによって、各画素を単独で二値化するよりも画質が良くなるんじゃね?っていう考え方です。
代表的な手法として
- 平均値制限法
- 平均誤差最小法
- 誤差拡散法
コレらが挙げられます。
その中でも今回の記事では平均値制限法の実装をしていきます!
平均値制限法について
ランダムティザ法では乱数によって閾値を決定、つまり外部から擾乱を与えているので画像がざらついたように見えますね。


なので平均値制限法では対象の画像が持っている濃淡のブレを利用して閾値を決めようみたいなノリでやっていきます。
具体的には対象の画素と、その近傍画素の濃度の平均値を閾値に用います。
座標(x,y)の画素とその近傍の画素の濃度の平均値をM(x,y)とすると、その画素の閾値T(x,y)を
T(x,y)=γ(K-1)+(1-2γ)M(x,y)
とします。
Kは階調数です。まあつまり256ですね。
γはこの閾値のバランスを決定するパラメータで0から0.5の値をとります。
γ=0.5の場合右の項が0になり、0.5×(256-1)=127.5と固定の値になります。なので普通の二値化と同じですね。
γ=0の場合は左の項が0になり、M(x,y)のみとなります。この時は近傍画素との平均値が閾値となるので画像そのものが持つブレを強調した形になります。
コードの実装
読み書きの関数
#include <iostream>
#include <opencv4/opencv2/opencv.hpp>
#define WIDTH 640
#define HEIGHT 480
using namespace std;
void readImage_gray( const char *fname, unsigned char img[HEIGHT][WIDTH])
{
cv::Mat ReadImage;
ReadImage = cv::imread(fname,0);
for( int w=0; w<WIDTH; w++ ){
for( int h=0; h<HEIGHT; h++ ){
img[h][w] = ReadImage.data[w+h*WIDTH];
}
}
}
void writeImage_binary( const char *fname, bool binary_img[HEIGHT][WIDTH])
{
cv::Mat writeImage( HEIGHT, WIDTH, CV_8U );
for( int w=0; w<WIDTH; w++ ){
for( int h=0; h<HEIGHT; h++ ){
if(binary_img[h][w])
writeImage.data[w+h*WIDTH]=0;
else
writeImage.data[w+h*WIDTH]=255;
}
}
cv::imwrite( fname , writeImage );
}
もし上の関数でわからないことがあれば下の記事を参考にしてください!
近傍画素との平均値を求める関数
とりあえずある画素とその近傍の画素の濃度値の平均を求める関数を作ります。
画像の内側の画素であれば近傍8画素が取れますが、画像の角や辺の画素では8画素取ることができない点を考慮したコードになるように工夫してます。
以下コードです
float getNeighborhoodAverage(unsigned char img[HEIGHT][WIDTH], int x, int y) {
// 3x3の領域の濃度値の合計を計算
int sum = 0.0f;
int count = 0;
for (int i = -1; i <= 1; ++i){
for (int j = -1; j <= 1; ++j){
if (x+j<0 || x+j>= WIDTH || y+i<0 || y+i>= HEIGHT){
// 範囲外の場合はスキップ
continue;
}
sum += (int)img[y+i][x+j];
++count;
}
}
// 平均値を計算して返す
return (float)(sum/count);
}
とりあえず引数はいつも通りグレースケール画像が格納されている二次元配列imgと対象画素の座標であるxとyです。
ちなみにWIDTHとHEIGTHはマクロ定義してある画像の横と縦の画素数です。
とりあえず最初に定義しているsumは対象画素と近傍画素の濃度値の合計を代入する変数ですね。
その次に定義しているcountは何個の画素の濃度値を足したかを記録する変数です。
これは最初に説明した通り画素の位置によって取れる近傍画素の数が変わるからですね。
てことで次の二重ループを見ていきましょう。
for (int i = -1; i <= 1; ++i){
for (int j = -1; j <= 1; ++j){
if (x+j<0 || x+j>= WIDTH || y+i<0 || y+i>= HEIGHT){
// 範囲外の場合はスキップ
continue;
}
sum += (int)img[y+i][x+j];
++count;
}
}
二重ループの条件文ではそれぞれiとjを-1から1まで変化するようにしてます。
これは対象画素を中心にして、近傍8画素と対象画素をラスタ走査しているイメージです。
その上でループの処理文を見ていきましょう。
if (x+j<0 || x+j>= WIDTH || y+i<0 || y+i>= HEIGHT){
// 範囲外の場合はスキップ
continue;
}
sum += (int)img[y+i][x+j];
++count;
if文によって画素が存在しない範囲に走査が行った場合を判別しています。
具体的にはx+jとy+iで現在注目している画素の位置が分かるので、その注目画素が0より小さくないか?画像のサイズより大きくないか?を判別します。
もしそうであればcontinueでループを一つ飛ばすようにしてます。
もし注目画素が画像の中にあればその濃度値をsumに足して、countをインクリメントしてループの中の処理文を終わらせます。
// 平均値を計算して返す
return (float)(sum/count);
返り値はsumをcountで割ったものにしてます。これで平均が出せるわけですね。
またこのしっかりfloatにキャストしておきましょう。
平均値制限法
そして次は二値化を行なっていく関数です!
void LimitTheMeanValue(unsigned char img[HEIGHT][WIDTH], bool binary_img[HEIGHT][WIDTH], float gamma){
for( int w=0; w<WIDTH; w++ ){
for( int h=0; h<HEIGHT; h++ ){
if(img[h][w]>(255*gamma+(1-2*gamma)*getNeighborhoodAverage(img,w,h)))
binary_img[h][w]=false;//白
else
binary_img[h][w]=true;//黒
}
}
}
引数は先ほどと同じく画像が入っているimg、二値化した画像を入れるbool型配列binary_img、閾値を算出する際に用いるパラメータγを格納するfloat型変数ganmaです。
処理の流れはいつも通りの二値化と同じですね。
とりあえず二重ループで画像をラスタ操作していきます。
その中の処理でif文を用いてその画素の濃度値が閾値より大きいかを判断します。
その際に先ほどの式と、平均値の関数を用いています。
if(img[h][w]>(255*gamma+(1-2*gamma)*getNeighborhoodAverage(img,w,h)))
binary_img[h][w]=false;//白
else
binary_img[h][w]=true;//黒
まあ式通りに書いているだけですね。
実際に使ってみた
それでは実行してみましょう。
今回も用いるのはこちらの画像

γ=0.5

γ=0.3

γ=0.1

γ=0.05

γ=0.03

γ=0

まとめ
いかがでしたか?
γの値を変えるだけで仕上がりが異なって面白いですね。
0.03ぐらいの時が一番良い感じですかね。
ではまたー
全体のコード
#include <iostream>
#include <opencv4/opencv2/opencv.hpp>
#define WIDTH 640
#define HEIGHT 480
using namespace std;
void readImage_gray( const char *fname, unsigned char img[HEIGHT][WIDTH])
{
cv::Mat ReadImage;
ReadImage = cv::imread(fname,0);
for( int w=0; w<WIDTH; w++ ){
for( int h=0; h<HEIGHT; h++ ){
img[h][w] = ReadImage.data[w+h*WIDTH];
}
}
}
void writeImage_binary( const char *fname, bool binary_img[HEIGHT][WIDTH])
{
cv::Mat writeImage( HEIGHT, WIDTH, CV_8U );
for( int w=0; w<WIDTH; w++ ){
for( int h=0; h<HEIGHT; h++ ){
if(binary_img[h][w])
writeImage.data[w+h*WIDTH]=0;
else
writeImage.data[w+h*WIDTH]=255;
}
}
cv::imwrite( fname , writeImage);
}
float getNeighborhoodAverage(unsigned char img[HEIGHT][WIDTH], int x, int y) {
// 3x3の領域の濃度値の合計を計算
int sum = 0.0f;
int count = 0;
for (int i = -1; i <= 1; ++i) {
for (int j = -1; j <= 1; ++j) {
if (x+j<0 || x+j>= WIDTH || y+i<0 || y+i>= HEIGHT) {
// 範囲外の場合はスキップ
continue;
}
sum += (int)img[y+i][x+j];
++count;
}
}
// 平均値を計算して返す
return (float)(sum/count);
}
void LimitTheMeanValue(unsigned char img[HEIGHT][WIDTH], bool binary_img[HEIGHT][WIDTH], float gamma){
for( int w=0; w<WIDTH; w++ ){
for( int h=0; h<HEIGHT; h++ ){
if(img[h][w]>(255*gamma+(1-2*gamma)*getNeighborhoodAverage(img,w,h)))
binary_img[h][w]=false;//白
else
binary_img[h][w]=true;//黒
}
}
}
int main(int argc, const char * argv[]) {
unsigned char img[HEIGHT][WIDTH]={0};
bool binary_img[HEIGHT][WIDTH] = {0};
cv::Mat showImage(HEIGHT, WIDTH, CV_8U );
readImage_gray("hana.png", img);
LimitTheMeanValue(img, binary_img,0);
writeImage_binary("Limit_binary_0.png", binary_img);
//showImage_binary(binary_img);
return 0;
}
コメント