はいどうもー
今回は二値化を扱います!
って言っても閾値を決めてそれより大きいか、小さいかで黒か白か決めていく単純な二値化ではなく、独立閾値決定法による二値化を実装します。
具体的にはランダムティザ法と組織ティザ法ですね。ていうことでやっていきましょう!
前提的なもの
前提として僕はXcodeでコードを書いて、実行しています。
opencvはhomebrewからダウンロードして、xcodeからパスをつないだり必要なファイルなどをブチ込んだり、、、とバンバンに使える状態から始めています。
Xcodeにopencvを導入する方法はgoogleで調べればいくらでもあるので、まだ導入できてない人はまずそこをお願いします。(気が向いたらこのサイトでもopencvの導入の記事を書くかも
普通の二値化の問題点
まず上でも説明しましたが単純な二値化はこんな感じですよね。
void binary(unsigned char img[HEIGHT][WIDTH], bool binary_img[HEIGHT][WIDTH]){
for(int w=0; w<WIDTH; w++){
for(int h=0; h<HEIGHT; h++){
if(img[h][w] < 180)
binary_img[h][w] = true;
else
binary_img[h][w] = false;
}
}
}
全ての画素に対して、その画素値がある閾値より上か下かで黒か白かを決めていきます。
実際に実行した結果がこちら↓


閾値にもよりますが、基本的にはこのよう感じになりますよね。

二値化はできているけど何ていうか、、
画像の大まかな構造が保存できてるけども、
表現的には微妙な気がする、、
そうなんですよね、
このような二値化ですと基本的に元の画像の濃淡表現が消えてしまいます。
まあ何の目的で二値化を行っているかにもよりますが、元の画像の構造をある程度残したい場合にはこの二値化は適切とは言えませんよね。
てことで今回考えるのが独立閾値決定法なんです!
独立閾値決定法とは
独立閾値決定法というのはその名の通り、画像の各画素に対して独立して閾値を決める方法のことです。
つまり画素値が0から255ある中で、ある画素に対しての閾値は100でまた別の画素に対しての閾値は200で、、、と言ったように閾値がそれぞれの画素に対して一つ決まっている感じなんですね。
閾値の設定が違うだけで二値化の手順は同じです。
閾値より大きければ白、小さければ黒と言ったように二値化画像を作っていくので、基本的に濃度値が大きいと黒に、小さいと白になる確率が高いというわけです。
なので基本的には画像の大まかな構造を捉えつつ、普通の二値化よりも濃淡の表現が出やすいことになります。
そして独立閾値決定法の中に閾値を決める方法がいくつかあり、今回はランダムティザ法と組織ティザ法というものを紹介したいと思います!
コードの実装
読み書きの関数
#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 );
}
こんな感じで画像はcv::Mat型ではなく、unsigned char型に画像を格納するようにしています。
また二値化した画像はbool型の配列に黒ならtrue、白ならfalseとして格納します。
もし上の関数でわからないことがあれば下の記事を参考にしてください!
ランダムティザ法
ランダムティザ法は各画素の閾値T(x,y)を一様乱数で決めてもある程度画像の構造が残るんじゃね?という考えから生まれた方法です。
グレースケールの画像の画素値の範囲である0〜255から一様に一つ選びそれを閾値とすれば良いってことですね。
この時の閾値がkである確率はどんなkに対しても1/256となります。
#define WIDTH 640
#define HEIGHT 480
void random_dither(unsigned char img[HEIGHT][WIDTH], bool binary_img[HEIGHT][WIDTH]){
std::random_device rnd;
for(int h=0; h<HEIGHT; h++){
for(int w=0; w<WIDTH; w++){
int k = rnd()%256;
if(img[h][w]>k)
binary_img[h][w] = false;//白
else
binary_img[h][w] = true;//黒
}
}
}
一つ目の引数のimgがグレースケールの画像が格納されている配列です。
二つ目の引数のbinary_imgは二値化した画像を格納するbool型の配列です。黒ならtrue、白ならfalseを格納します。
本文の1行目で閾値を決めるために用いる乱数の生成を行うためにstd::random_deviceを用いています。(rand()より一様になるっぽいんでこちらを採用しました。)
そこから二重ループで画像をラスタ走査していきます。
その後各画素に対して、毎回一行目で作った乱数生成器で乱数をint型のkに代入します。
その値を256で割った余り(0〜255のいずれかの値)より注目している画素が大きいか小さいかをif文で判定して、binary_imgの対応する位置にtrueかfalseを入れている感じですね。
組織ティザ法
次は組織ティザ法です。
こっちは先ほどのように一つの画素に対してそれぞれ閾値を決めるのではなく、画像を小ブロックに分割してそのブロックに対して閾値行列というものを当てはめて各画素の閾値を決定します。
こんなイメージです↓

このようにすることで均一に閾値を散らせるわけですね。
閾値行列を設定する際にはティザ行列というものを利用します。
ティザ行列には大きさやパターンで何種類かあるのですが、今回の記事では以下の二つを紹介します。

4×4ベイヤー型も4×4網点型も見て分かる通り0〜15の値がうまく散らされている行列です。
コレを用いて17段階(4×4+1)の閾値を作成します。
画像の画素値が0〜255であるため255/17=15という計算から、ティザ行列Dに対して(D+1)x15とすることで閾値行列にすることができます。

てことで実装していきます。
void ordered_dither(unsigned char img[HEIGHT][WIDTH], bool binary_img[HEIGHT][WIDTH], int mode){
unsigned char Bayer[4][4]={{0,8,2,10},
{12,4,14,6},
{3,11,1,9},
{15,7,13,5}};
unsigned char halftone_dot[4][4]={{0,2,14,12},
{8,10,5,7},
{15,13,1,3},
{4,6,9,11}};
unsigned dither[4][4]={0};
if(mode==0){
for(int i=0; i<4; i++)
for(int j=0; j<4; j++)
dither[i][j] = Bayer[i][j];
}else if(mode==1){
for(int i=0; i<4; i++)
for(int j=0; j<4; j++)
dither[i][j] = halftone_dot[i][j];
}
for(int h=0; h<HEIGHT; h+=4){
for(int w=0;w<WIDTH; w+=4){
for(int i_h=h, i=0; i<4&&i_h<HEIGHT; i_h++,i++){
for(int j_w=w, j=0; j<4&&j_w<WIDTH; j_w++,j++){
if(img[i_h][j_w]>((int)dither[i][j]+1)*15)
binary_img[h][w] = false;//白
else
binary_img[h][w] = true;//黒
}
}
}
}
}
引数の前二つは先ほどの関数と同じですね。
3つ目のint型のmodeはベイヤー型か網点型のどちらかを使うかを指示するための変数です。
本文ではまず4×4のベイヤー型、網点型のティザ行列を定義しています。
unsigned char Bayer[4][4]={{0,8,2,10},
{12,4,14,6},
{3,11,1,9},
{15,7,13,5}};
unsigned char halftone_dot[4][4]={{0,2,14,12},
{8,10,5,7},
{15,13,1,3},
{4,6,9,11}};
その次は4×4の配列に、渡されたmodeに合わせてティザ行列を閾値行列に変換して代入していきます。
unsigned char dither[4][4]={0};
if(mode==0){
for(int i=0; i<4; i++)
for(int j=0; j<4; j++)
dither[i][j] = (Bayer[i][j]+1)*15;
}else if(mode==1){
for(int i=0; i<4; i++)
for(int j=0; j<4; j++)
dither[i][j] = (halftone_dot[i][j]+1)*15;
}
そしてここからが少しややこしいんですよね。
今回も記事では最初から画像の縦と横サイズをマクロ定義でHEIGHTとWIDTHとして480、640と定義してました。閾値行列の大きさも4×4なので、この閾値行列で480×640の画像をブロックで分割しようとすると余りなく綺麗に分割できます。
しかし、さまざまな画像のサイズに対応できるように分割しきれなかった場合にも対応できるようなコードにしていきます。
for(int h=0; h<HEIGHT; h+=4){
for(int w=0;w<WIDTH; w+=4){
for(int i_h=h, i=0; i<4&&i_h<HEIGHT; i_h++,i++){
for(int j_w=w, j=0; j<4&&j_w<WIDTH; j_w++,j++){
if(img[i_h][j_w]>dither[i][j])
binary_img[h][w] = false;//白
else
binary_img[h][w] = true;//黒
}
}
}
}
まずは外側の二重forループから
for(int h=0; h<HEIGHT; h+=4){
for(int w=0;w<WIDTH; w+=4){
///////////////////////
}
}
普段のループとは違い、縦も横も4ずつインクリメントしていきます。
ここのイメージはこんな感じです↓

赤点のところを走査している感じですね。
このforループで各ブロックの(0,0)座標に飛んでいるイメージです。
そして次は中のループです
for(int i_h=h, i=0; i<4&&i_h<HEIGHT; i_h++,i++){
for(int j_w=w, j=0; j<4&&j_w<WIDTH; j_w++,j++){
if(img[i_h][j_w]>dither[i][j])
binary_img[h][w] = false;//白
else
binary_img[h][w] = true;//黒
}
}
この二つのループでブロック内をラスタ操作している感じですね。
一番外側のfor文について詳しく見ていきましょう。
初期条件 int i_h=h, i=0;
初期条件としてint i_h=h, i=0;となっています。
j_wは画像全体の位置を把握するための変数です。
対してjはブロック内の位置を把握するための変数です。
継続条件 i<4&&i_h<HEIGHT;
継続条件はi<4&&i_h<HEIGHTとなっています。
i<4というのは走査がブロックからでないようにするためですね。
i_h<HEIGHTは全体としての走査が画像からはみ出ないようにするためです。これはブロックで分割した際に余りとなった領域に対して効く条件ですね。
変化式 i_h++,i++
変化式はi_h++,i++となっています。
ブロック内としても、全体としても注目する画素を一つずらすという意味ですね。
処理
ループの中ではランダムティザの時と同じように、各画素に対して閾値の処理を行います。
その際に用いる閾値が最初に定義した閾値行列になります。
実際に使ってみた
こちらの画像で上の二値化を試してみました。

ランダムティザ法

組織ティザ法(ベイヤー型)

組織ティザ法(網点型)

まとめ
上の3つの二値化いかがだったでしょうか?
ランダムティザ法は良い感じですね。組織ティザのベイヤー型の方もある程度濃淡が残っていますし、少し階調をいじればより良くなると思います。
てことで独立閾値決定法による二値化でした!
ではまたー
全体のコード
#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 );
}
void binary(unsigned char img[HEIGHT][WIDTH], bool binary_img[HEIGHT][WIDTH]){
for(int w=0; w<WIDTH; w++){
for(int h=0; h<HEIGHT; h++){
if(img[h][w] < 180)
binary_img[h][w] = true;
else
binary_img[h][w] = false;
}
}
}
void random_dither(unsigned char img[HEIGHT][WIDTH], bool binary_img[HEIGHT][WIDTH]){
std::random_device rnd;
for(int h=0; h<HEIGHT; h++){
for(int w=0; w<WIDTH; w++){
int k = rnd()%256;
if(img[h][w]>k)
binary_img[h][w] = false;//白
else
binary_img[h][w] = true;//黒
}
}
}
void ordered_dither(unsigned char img[HEIGHT][WIDTH], bool binary_img[HEIGHT][WIDTH], int mode){
unsigned char Bayer[4][4]={{0,8,2,10},
{12,4,14,6},
{3,11,1,9},
{15,7,13,5}};
unsigned char halftone_dot[4][4]={{0,2,14,12},
{8,10,5,7},
{15,13,1,3},
{4,6,9,11}};
unsigned char dither[4][4]={0};
if(mode==0){
for(int i=0; i<4; i++)
for(int j=0; j<4; j++)
dither[i][j] = (Bayer[i][j]+1)*15;
}else if(mode==1){
for(int i=0; i<4; i++)
for(int j=0; j<4; j++)
dither[i][j] = (halftone_dot[i][j]+1)*15;
}
for(int h=0; h<HEIGHT; h+=4){
for(int w=0;w<WIDTH; w+=4){
for(int i_h=h, i=0; i<4&&i_h<HEIGHT; i_h++,i++){
for(int j_w=w, j=0; j<4&&j_w<WIDTH; j_w++,j++){
if(img[i_h][j_w]>dither[i][j])
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};
readImage_gray("hana.png", img);
random_dither(img,binary_img);
//ordered_dither(img,binary_img,1);
writeImage_binary("binary.png",binary_img);
return 0;
}
コメント