(輝度)ヒストグラムとレベル補正

最終更新:2009年2月5日

(輝度)ヒストグラム

以下のファイルを自分の作業ディレクトリにコピーしてください.

> cp ~fukunaga/ipwork/tmuinthouse.pgm ./

> xv tmuinthouse.pgm

このコマンド最後に&をつけてもつけなくても構いません.

としてこの写真を鑑賞してください.国際交流会館の1階部分です.ふくなが撮影です.黒(輝度0)から白(輝度255)までだいたいまんべんなく輝度が分布しているように思われます.多少写真右上に白抜けの部分が広がっているのでその輝度が他を凌駕しているかもしれません.この輝度分析を行ってみましょう.

この画像のヒストグラム解析を行います.

以下のプログラムを読みましょう.pgmファイル(初期値として与える)を解析してすべてのピクセルのグレー度(0から255)を解析しその頻度を標準出力に出力します.

//import ppmread.class ;
class ppmHistdata {
    public static void main(String args[]) {
        int i,j,k ;
        int histdata[] ;
        ppmread testf = new ppmread() ;
        testf.ppmmain(args[0]) ;
        System.out.println("# Data created by ppmHistdata") ;
        System.out.println("# input filename = "+ testf.sfilename ) ;
        System.out.println("# Width    = "+testf.width) ;
        System.out.println("# Height   = "+testf.height) ;
        if(testf.magicidno>1) System.out.println("# Maxval   = "+testf.maxval) ;
        histdata=new int [testf.maxval+1] ;
        
        //testf.ppmHeaderInfo() ;
        
        for(i=0;i<testf.height;i++) 
            for(j=0;j<testf.width;j++) {
                if(testf.gdata[j][i]>(short)1000) 
                    System.out.println("Error data = "+testf.gdata[j][i]);
                histdata[(int)testf.gdata[j][i]] += 1 ;
            }
        int accum = 0 ;
        for(i=0;i<testf.maxval+1;i++) {
            System.out.print(histdata[i]) ;
            accum += histdata[i] ;
            System.out.println(" "+accum) ;
            System.out.println() ;      
         }
        
    }
}

なおこのプログラムにはppmread.javaが必要です.

> cp ~fukunaga/ipwork/ppmread.java ./

> cp ~fukunaga/ipwork/ppmHistdata.java ./

そしてコンパイル

> javac ppmread.java (これは1度だけ)

> javac ppmHistdata.java

> java ppmHistdata tmuinthouse.pgm

としてみてください.

# Data created by ppmHistdata
# input filename = tmuinthouse.pgm
# Width    = 480
# Height   = 360
# Maxval   = 255
14 14
11 25
17 42
21 63
19 82
22 104
15 119
33 152
29 181
39 220
61 281
75 356
113 469
188 657
277 934
415 1349
537 1886
563 2449
522 2971
509 3480
471 3951
.....

とでてきます.#で示されるヘッダーデータの後の各行2つの数字のうち最初はこは輝度0,1,2,....,255までを示すピクセルの数です.2番目はそこまでの累積値です.ピクセルの総数は480×360です.この数字の羅列ではなんだか分かりづらいのでこんどは実際にヒストグラムを作成します.その前にデータをファイルへリダイレクトします.そしてgnuplotを使いましょう.

> java ppmHistdata tmuinthouse.pgm > tmuinthouse.dat

> gnuplot

するとロゴ画面が現れます.以下のよう.

        G N U P L O T
        Version 4.0 patchlevel 0
        last modified Thu Apr 15 14:44:22 CEST 2004
        System: Darwin 8.6.0
        Copyright (C) 1986 - 1993, 1998, 2004
        Thomas Williams, Colin Kelley and many others
        This is gnuplot version 4.0.  Please refer to the documentation
        for command syntax changes.  The old syntax will be accepted
        throughout the 4.0 series, but all save files use the new syntax.
        Type `help` to access the on-line reference manual.
        The gnuplot FAQ is available from
                http://www.gnuplot.info/faq/
        Send comments and requests for help to
                <gnuplot-info@lists.sourceforge.net>
        Send bugs, suggestions and mods to
                <gnuplot-bugs@lists.sourceforge.net>
Terminal type set to 'x11'
gnuplot> 

と出てきます.

いろいろ様々なグラフィックプロットができるのですが、それらはgnuplotのサイト(英語だけど問題ない)とか使い方の解説がとてもためになる米国ロスアラモス研究所のKawanoさんのサイト龍谷大学の樋口さんのサイトなどです.しかしここではそのような寄り道はせずにヒストグラムのプロットのみにgnuplotを利用してみましょう.さきのkawanoさんのサイトのヒストグラム処理についての解説を参考にしてください.

gnuplot > plot "tmuinthouse.dat" using 0:1 with boxes

とします.plot "file_name" with boxesの形については米国ロスアラモス研究所のKawanoさんのサイトからデータファイルの数値プロットという項をクリックして意味を参照してください.

usingというオプションについてはgnuplotのhelp plotでその意味の全貌が分かりますがちょっとややっこしいのでこのヒストグラム解析で必要な部分のみをここで解説します.1行に2つ以上のデータが(スペース,タブで分たれて)記録されている場合,using n:mとしてn列目のデータをx,m列目のデータをyとしてグラフを作成します.nもmも1行に書かれている記録総数以下でなければなりませんが,nとmの大小関係に制限はありません.このnやmに0(ゼロ)を指定できます.その0列目には0から行番号が記録されてると想定されます.したがってusing 0:mとするとxは行番号単位となりそれごとのm列目のデータをプロットするということになります.

なおusing 0:1という形をとる場合とusing 1という形でも良い場合もありますが,それはもう一つのwithで指定するプロット表現オプションによるようです.with boxesではusing 0:1あるいはusing 0:2というように指定します.

とでてきます.グラフィックスの表示の関係で一定間隔で空白がありますが、問題ありません.実際はデータが詰まっています.表示の問題です.x軸は左から右に黒→白となっています.250あたりのピークは図の右上の白抜けの部分に対応するのでしょう.

階調変更

ピクセルマップのデータを読んでそのデータを変更させながら明るい画像、暗い画像、コントラストについて見ていきましょう.

まず以下のプログラムから.~fukunaga/ipwork/ppmImagBrighter.java

//import ppmread.class ;
class ppmImagBrighter {
    public static void main(String args[]) {
        int i,j,k ;
        short imageData[][] ;
        ppmread testf = new ppmread() ;
        double mfactor ;
        short newLow=50 ;
        testf.ppmmain(args[0]) ;
        if(args.length > 1)
            newLow = (short)Integer.parseInt(args[1]) ;
        mfactor = (double)(255-(int)newLow)/(double)255 ;
        //      System.out.println("filename = "+ testf.sfilename ) ;
        //System.out.println("Width    = "+testf.width) ;
        //System.out.println("Height   = "+testf.height) ;
        //if(testf.magicidno>1) System.out.println("Maxval   = "+testf.maxval) ;
        imageData=new short [testf.width][testf.height] ;
        //testf.ppmHeaderInfo() ;
        System.out.println("P"+testf.magicidno) ;
        System.out.println("# "+testf.sfilename+" is modified Brighter") ;

        System.out.print(testf.width+" "+testf.height) ;
        if(testf.magicidno >1 ) System.out.println(" "+testf.maxval) ;
        for(i=0;i<testf.height;i++) 
            for(j=0;j<testf.width;j++) {
                if(testf.gdata[j][i]>(short)1000) 
                    System.out.println("Error data = "+testf.gdata[j][i]);
                double xfactor = mfactor*(double)(testf.gdata[j][i]) ;
                imageData[j][i]=(short)(Math.round(xfactor)); 
                imageData[j][i] += newLow ;
            }
        int ndataMaxLine=50 ;
        int rdataline,rdatalast ;
        rdataline = testf.width/ndataMaxLine ;
        rdatalast = testf.width%ndataMaxLine ;
        for(k=0;k<testf.height;k++) {
            for(i=0;i<rdataline;i++) {
                for(j=0;j<ndataMaxLine;j++) {
                    int ij=i*ndataMaxLine+j ;
                    System.out.print(imageData[ij][k]+" ") ;
                }
                System.out.println() ;
            }
            for(j=0;j<rdatalast;j++) {

                int ij=rdataline*ndataMaxLine+j ;
                System.out.print(imageData[ij][k]+" ") ;
            }
            System.out.println() ;
        }
        //      for(i=0;i<testf.maxval+1;i++)
        //          System.out.println(histdata[i]) ;
    }
}

これをコンパイルしてtmuinthouse.pgmのデータを全体的に明るくさせます.

> java ppmImagBrighter tmuinthouse.pgm 100 > tmuinthousehigh.pgm

としてみます.ここで100として与えているのは、ヒストグラムの下限値を100とするということです.すると画像は以下のように変わります.

いままで0から255までの階調は明るい方(100,255)に圧縮されます.このヒストグラムをとると以下のようです.

暗くするには以下のプログラムを使います.~fukunaga/ipwork/ppmImagDarker.java.

//import ppmread.class ;
class ppmImagDarker {
    public static void main(String args[]) {
        int i,j,k ;
        short imageData[][] ;
        ppmread testf = new ppmread() ;
        double mfactor ;
        short newHigh=50 ;
        testf.ppmmain(args[0]) ;
        if(args.length > 1)
            newHigh = (short)Integer.parseInt(args[1]) ;
        mfactor = (double)newHigh/(double)255 ;
        //      System.out.println("filename = "+ testf.sfilename ) ;
        //System.out.println("Width    = "+testf.width) ;
        //System.out.println("Height   = "+testf.height) ;
        //if(testf.magicidno>1) System.out.println("Maxval   = "+testf.maxval) ;
        imageData=new short [testf.width][testf.height] ;
        //testf.ppmHeaderInfo() ;
        System.out.println("P"+testf.magicidno) ;
        System.out.println("# "+testf.sfilename+" is modified Brighter") ;

        System.out.print(testf.width+" "+testf.height) ;
        if(testf.magicidno >1 ) System.out.println(" "+testf.maxval) ;
        for(i=0;i<testf.height;i++) 
            for(j=0;j<testf.width;j++) {
                if(testf.gdata[j][i]>(short)1000) 
                    System.out.println("Error data = "+testf.gdata[j][i]);
                double xfactor = mfactor*(double)(testf.gdata[j][i]) ;
                imageData[j][i]=(short)(Math.round(xfactor)); 
            }
        int ndataMaxLine=50 ;
        int rdataline,rdatalast ;
        rdataline = testf.width/ndataMaxLine ;
        rdatalast = testf.width%ndataMaxLine ;
        for(k=0;k<testf.height;k++) {
            for(i=0;i<rdataline;i++) {
                for(j=0;j<ndataMaxLine;j++) {
                    int ij=i*ndataMaxLine+j ;
                    System.out.print(imageData[ij][k]+" ") ;
                }
                System.out.println() ;
            }
            for(j=0;j<rdatalast;j++) {
                int ij=rdataline*ndataMaxLine+j ;

                System.out.print(imageData[ij][k]+" ") ;
            }
            System.out.println() ;
        }
        //      for(i=0;i<testf.maxval+1;i++)
        //          System.out.println(histdata[i]) ;
    }
}

> java ppmImagBrighter tmuinthouse.pgm 150 > tmuinthouselow.pgm

このプログラムによりヒストグラム(つまり元データ)は0から150と暗い方に圧縮されます.その結果が以下のようです.

そしてヒストグラムは

これら2つのプログラムで明るくしたり暗くしたりしました.

課題1

それでは上記のプログラムを一つにまとめ1つのプログララムで下限値を上げられ、かつ上限値を下げられるようにしてください.そして下限値と上限値を狭めいわゆるコントラストの低い画像を作り出してみてください.そのヒストグラムを作成してください.

javaファイル名はppmImagContrast.java、作り上げる画像をtmuinthouselowcon.pgmとします.

課題2

こんどはtmuinthouselowcon.pgmの低いコントラスト画像を高めるよう(もとに戻すよう)にしてください.上記のppmImagContrastでは狭めることはできた.しかしもともとコントラストの低い(明度分布の狭い)画像データの明度分布を広げるためには工夫(だまし)が必要、どうするか?

課題3

ppmImagContrast.javaを再び改造し、もうひとつ変更パラメータを加えます.以下のようにグレイレベル中間点が移動できるようにしてください.

2値化処理(Binarization)

階調変換の特殊なものに2値化処理というものがある.256段階に分布している階調を背景(0)と対象(1)のみに分類し新たな画像イメージ(0と1しかないので、ppm、pgmファイル→pbmファイルに変換される)を作り上げようとするものである.画像中の特定の領域のみを取り出して形状解析などをする場合に利用される手段です.多値にわたっている階調をある閾値を境に0と1に分けてしまうので閾値処理(Thresholding)ともいわれています.階調分布領域中のある固定値Tを閾値として画像f(i,j)を新たなg(i,j)に変換するのに以下のような処理をさせようとするものです.

g(i,j) = 1 if f(i,j)> T,

g(i,j) = 0 else if f(i,j) ≦ T

いうまでもなくこの処理で一番大事なところは画像の閾値Tを見いだすところです.一般的には具体的な個々の画像のヒストグラムから値を算出させるなどの閾値決定のアルゴリズムもある程度開発されています.大津展之氏の開発した判別法を用いた閾値算出法が使われたりします.これはヒストグラム(濃淡分布)をもとにします.そのヒストグラムを閾値をもとに2クラスに分割したときにクラス間分散が最大になる値を閾値とするものです.以下の図ようにそれぞれのクラスの平均(m0,m1)、全体の平均(m)を表す.

この時のクラス間分散を以下のように定義する.このときa(b)は閾値より小(以上)に属するピクセル総数を全ピクセル数で割ったものである.

具体的にはTを全領域で変化させてクラス間分散を計算、それが最大を示すTの値を閾値とすればよい.その計算をするプログラムを以下に示します.

~fukunaga/ipwork/ppmHistDiv2ThFind.java

//import ppmread.class ;
class ppmHistDiv2ThFind {
    public static void main(String args[]) {
        int i,j,k,T0,T1 ;
        int histdata[] ;
        ppmread testf = new ppmread() ;
        testf.ppmmain(args[0]) ;
        System.out.println("# Data created by ppmHistDiv2ThFind") ;
        System.out.println("# input filename = "+ testf.sfilename ) ;
        System.out.println("# Width    = "+testf.width) ;
        System.out.println("# Height   = "+testf.height) ;
        if(testf.magicidno>1) System.out.println("# Maxval   = "+testf.maxval) ;
        histdata=new int [testf.maxval+1] ;
        
        T0=0 ;
        T1=255 ;
        if(args.length>1) {
            T0=Integer.parseInt(args[1]) ;
            if(T0<0 || T0 >testf.maxval) {
                System.err.println("Incorrect Threshold value T= "+T0) ;
                System.exit(1) ;
            }
        }
        if(args.length>2) {
            T1=Integer.parseInt(args[2]) ;
            if(T1<0 || T1 >testf.maxval) {
                System.err.println("Incorrect Threshold value T= "+T1) ;
                System.exit(1) ;
            }
        }
        if(T1<T0) {
            int w ;
            w=T1 ;
            T1=T0;
            T0=w ;
        }
        double sigmax=0.0,m1max=0.0,m0max=0.0,mT=0.0 ;
        int thmax=0 ;
        for(int ii=T0;ii<T1+1;ii++) {
            double m0=0.0,m1=0.0;
            int n0=0,n1=0,nT=0 ;
            mT=0.0 ;
            for(i=0;i<testf.height;i++) 
                for(j=0;j<testf.width;j++) {
                    if(testf.gdata[j][i]>(short)1000) 
                        System.out.println("Error data = "+testf.gdata[j][i]);
                    mT += testf.gdata[j][i] ;
                    nT++ ;
                    if(testf.gdata[j][i]>ii) {
                        m0 += testf.gdata[j][i] ;
                        n0++ ;
                    } else if(testf.gdata[j][i]<=ii) {
                        m1 += testf.gdata[j][i] ;
                        n1++ ;
                    }
                }
            double a,b,sigma ;
            a = (double)n0/nT ;
            b = (double)n1/nT ;
            m0=m0/n0 ;
            m1=m1/n1 ;
            mT=mT/nT ;
            sigma = a*(m0-mT)*(m0-mT)+b*(m1-mT)*(m1-mT) ;
            if(sigmax<sigma) {
                sigmax=sigma ;
                thmax = ii ;
                m0max=m0;
                m1max=m1;
            }
            System.out.println(
                               "(¥t"+ii+") sigma= "+sigma) ;
        }
        System.out.println("¥nsigma max = "+sigmax) ;
        System.out.println("Thhreshold = "+thmax) ;
        System.out.println("Mean 0 max = "+m0max) ;
        System.out.println("Mean 1 Max = "+m1max) ;
        System.out.println("Mean Total = "+mT) ;
    }
    
}

> javac ppmHistDiv2ThFind.java

実際の計算では最初のパラメータで対象となる画像データファイルの名前、2番目、3番目のパラメータで閾値の最大値をスキャンする範囲(最小値、最大値)をあたえます.例えば

> java ppmHistDiv2ThFind tmuinthouse.pgm 50 250

とするとその最後の出力部分は以下のようになります.

(       239) sigma= 1605.5820908942828
(       240) sigma= 1592.9069816762049
(       241) sigma= 1580.3339168543082
(       242) sigma= 1570.3620772554582
(       243) sigma= 1558.3174406015717
(       244) sigma= 1546.6093703243855
(       245) sigma= 1534.2109419777325
(       246) sigma= 1519.3135885739857
(       247) sigma= 1499.2908957475574
(       248) sigma= 1475.1104002710663
(       249) sigma= 1448.7697633524244
(       250) sigma= 1412.1416345990426
sigma max = 2677.3831750435093
Thhreshold = 125
Mean 0 max = 177.40952779968455
Mean 1 Max = 73.73122163058659
Mean Total = 122.4216261574074

これにより閾値は125となることがわかりました.m0,m1は73.7,177.4とそれぞれなりました.それをヒストグラム中で図示すると以下のようになります.

これをもとにT=220,125の2つの閾値で計算して変換した画像を以下に示します.この2値化プログラムのソースコードはここではのせませんが(以下の課題4の関係)、ppmBinarize.classはあります.コピーして(> cp ~fukunaga/ipwork/ppmBinarize.class ./)、以下のように実行させてみてください.

> java ppmBinarize tmuinthouse.pgm 220 > tmuinthouse220.pbm

> java ppmBinarize tmuinthouse.pgm 125 > tmuinthouse125.pbm

T=220
T=125

課題4

ppmBinarize.javaを作ってください.起動時にあたえるパラメータは2つ.1つはもとになる画像ファイル、もうひとつのパラメータは閾値です(0から255まで).なお閾値より高い時は0、低い時は1と設定してください.

ラベリング(ラベル化)

2値化した画像は通常画像内のオブジェクトの認識、抽出に利用されます.このため画素単位でそれがどのオブジェクトに属しているかの情報を付加させることを行います.各画素に以下の図のようなグループ情報をつけることをラベリング、あるいはラベル化と呼びます.連結した画素に同じラベル(番号)をつけることにより複数の領域をグループ分けします.

いくつかの方法がありますがここでは以下のようなマスクパターンを利用したラベリング処理の実際をみていきましょう.今画像の原点(左上コーナ)から右向き水平方向、そして下向き垂直方向にラベリングのためのスキャン(走査)を行ってきたとします.そして今スキャンはまさに黄色の画素の処理を行うところとします.x0からx3までのラベリングはなされていることに注意してください.これらの情報を使って現行画素の処理を行います.

ここまで準備をして以下のステップで処理はなされます(以下の文章で画素とピクセルということばがでてきますが同じものと考えてください、文学的文章と同じくたまに(雰囲気で)違う名前で呼んでみたかったりしただけです).

  1. 全画素分1対1に対応するラベルマトリックスを用意します.ldata[j,i], j<width,i<height、ラベルλを格納します.このマトリックスを最初にゼロにリセット.上図のマスクはプログラム中ではlabelmask[i],i=0,3とします.作業配列Tを用意します.作業途中のラベル候補数の予想される最大値+α分とっておきます.
  2. 左上の原点画素データからスキャン開始.データが1に対応する画素のみに以下のステップ3を実行します.
  3. 現行画像の座標をj,iとします.まずラベルマスクlabelmask[k],k=0,3に既にラベリングされたピクセルのldata[j-1,i-1], ldata[j,i-1], ldata[j+1,i-1], ldata[j-1][i]の値をセット.この4つのピクセル内に登録されるラベルの数は最大2種類なのですが作業中には頻繁に3個の異なるラベルがマスク内にセットされることもあります.そのラベルの数(n)によって以下のように処理が分かれます.小さいラベル番号からL1,L2,(L3)とします.
  1. 全画像のスキャンを終えます.
  2. こんどは全画素のラベルλ=ldata[j,i]を見直します.そのラベルT(λ)の値を調べて、その値をldata[j,i]に代入し直します.それはスキャンしていく過程で最初あるラベルが付与されたけれど、スキャンがさらに進行するに従って他のラベルに改名しなおさなければならなかった場合がステップ3、case 2,3,4であったからです.この整理を行います.

以上でラベリングが完了します.

ファイルは~fukunaga/ipwork/ppmLabeling.javaです.

//import ppmread.class ;
//quickSort.java
class ppmLabeling {
    public static void main(String args[]) {
        int i,j,k ;
        int Maxpattern=255 ;
        ppmread image = new ppmread() ;
        int ldata[][] ;
        int labelmask[] = new int[4] ;
        int labelmasksorted[] = new int[4] ;
        int T[] = new int[Maxpattern] ;
        int Llist[] = new int [5]  ;
        int lambda,n ;
        image.ppmmain(args[0]) ;
        System.out.println("P2 "+image.width+" "+image.height+" 255") ;
        System.out.println("# created by ppmLabeling") ;
        System.out.println() ;
        quickSort q = new quickSort() ; 
        ldata=new int [image.width][image.height] ;
        //Step 1
        lambda=0 ;

        for(i=0;i<image.height;i++) {
            for(j=0;j<image.width;j++) {
                //Step 2
                if(image.gdata[j][i] == 1) {
                    //Step 3  
                    if(i==0 && j==0) {                    // top-left corner
                        for(k=0;k<4;k++) labelmask[k]=0 ;
                    } else if(i==0 && j>0) {              // top line
                        for(k=0;k<3;k++) labelmask[k]=0 ;
                        labelmask[3]=ldata[j-1][i] ;
                    } else if(i>0  && j==0) {             // left-line
                        labelmask[0] = 0;
                        labelmask[3] = 0;
                        labelmask[1] = ldata[0][i-1] ;
                        labelmask[2] = ldata[1][i-1] ;
                    } else if(i>0 && j==(image.width-1)) {//right-line
                        labelmask[2] = 0 ;
                        labelmask[0] = ldata[j-1][i-1] ;
                        labelmask[1] = ldata[j][i-1] ;
                        labelmask[3] = ldata[j-1][i] ;
                    } else {                              //elsewhere
                        labelmask[0] = ldata[j-1][i-1] ;
                        labelmask[1] = ldata[j][i-1] ;
                        labelmask[2] = ldata[j+1][i-1] ;
                        labelmask[3] = ldata[j-1][i] ;
                    }
                    n=0 ;
                    for(k=0;k<4;k++) {
                        Llist[k]=0 ;
                        labelmasksorted[k]=labelmask[k] ;
                    }
                    q.quicksort(labelmasksorted,0,3) ;
                    for(k=0;k<4;k++) 
                        if(labelmasksorted[k] != 0)
                            if(n == 0) Llist[n++]=labelmasksorted[k] ; 
                            else if(n > 0)
                                if(labelmasksorted[k] != Llist[n-1]) 
                                    Llist[n++] = labelmasksorted[k] ;
                    
                    if(n>2) {
                        System.err.print("j= i= "+j+" "+i+" |") ;
                        System.err.print
                            (" n = "+n+" L1 = "+Llist[0]+" L2 = "+Llist[1]) ;
                        System.err.print(" Too much patterns in mask \n") ;
                    }
                    switch (n) {
                    case 0:
                        lambda++ ;
                        T[lambda] = lambda ;
                        ldata[j][i] = lambda ;
                        break ;
                    case 1:
                        if(labelmask[0]!=0&&labelmask[1]==0&&labelmask[2]==0
                           &&labelmask[3]==0) {
                            lambda++ ;
                            T[lambda] = lambda ;
                            ldata[j][i] = lambda ;
                            break ;
                        }
                        ldata[j][i] = Llist[0] ;
                        break ;
                    case 2:
                        if(labelmask[0]==0&&labelmask[1]==0&&labelmask[2]!=0
                           &&labelmask[3]!=0) {
                            ldata[j][i]=ldata[j-1][i];
                            break ;
                        }
                        ldata[j][i]=Llist[0] ;
 
                        for(int c=0;c<lambda+1;c++) 
                            if(T[c] == Llist[1]) 
                                T[c]=Llist[0] ;
                        break ;
                    case 3:
                        ldata[j][i]=Llist[0] ;
                        for(k=0;k<2;k++) 
                            for(int c=0;c<lambda+1;c++) 
                                if(T[c] == Llist[k+1]) T[c]=Llist[0] ;
                        break ;
                    case 4:
                        ldata[j][i]=Llist[0] ;
                        for(k=0;k<3;k++) 
                            for(int c=0;c<lambda+1;c++) 
                                if(T[c] == Llist[k+1]) T[c]=Llist[0] ;
                        break ;
                    }
                }
                //Step 4
            }
        }
        //Step 5
        for(i=0;i<image.height;i++) 
            for(j=0;j<image.width;j++)
                if(ldata[j][i] != 0) 
                    ldata[j][i]=T[ldata[j][i]] ;
        int ndataMaxLine=50 ;
        int rdataline,rdatalast ;
        rdataline = image.width/ndataMaxLine ;
        rdatalast = image.width%ndataMaxLine ;
        for(k=0;k<image.height;k++) {
            for(i=0;i<rdataline;i++) {
                for(j=0;j<ndataMaxLine;j++) {
                    int ij=i*ndataMaxLine+j ;
                    System.out.print(ldata[ij][k]+" ") ;
                }
                System.out.println() ;
            }
            for(j=0;j<rdatalast;j++) {
                int ij=rdataline*ndataMaxLine+j ;
                System.out.print(ldata[ij][k]+" ") ;
            }
            System.out.println() ;
        }
        System.err.println("\n# Number of pattern initially found is "+lambda) ;
        k=0 ;
        for(i=1;i<=lambda;i++) if(T[i] != i) k++ ;
        System.err.println("# final pattern survived is "+(lambda-k)) ;
        System.err.println("\n#-- pattern Number List --") ;
        for(i=1;i<=lambda;i++) {
            if(T[i]==i) System.err.print("# "+i+" "+T[i]) ;
            if(T[i] == i) System.err.println(" Survived") ; 
        }
    }
        
}
        

題材として以下の画像を処理してみましょう:

ファイルは~fukunaga/ipwork/twovaluesimple.ppmです.

> java ppmBinarize twovaluesimple.ppm 200 > twovaluesimple.pbm

としてtwovaluesimple.pbm(ビットマップ)は

これをラベリング処理します.

> java ppmLabeling twovaluesimple.pbm > twovaluesimple.pxm

この処理を行うと(マスクラベルに3個以上のラベルが含まれるという)ワーニングメッセージが出されますが最後に以下のようなメッセージが出力され最終的にラベル化により3種類のラベル(グループ)1、7、8が生き残ったのだとわかります.

ここで注意しておきます.このプログラムのリダイレクト先のファイル名の最後の部分は.pxmになっています.これはppm(Portable Pixel Map)形式ではないものです.ファイルの先頭部はP1となってpbm形式にも見えますが、1とあるべきところにラベル化された数字がならんでいます.これはグラフ出力ソフトでは処理できないものであることに注意してください.

j= i= 60 87 | n = 3 L1 = 3 L2 = 6 Too much patterns in mask 
j= i= 181 142 | n = 3 L1 = 15 L2 = 56 Too much patterns in mask 
j= i= 415 142 | n = 3 L1 = 8 L2 = 17 Too much patterns in mask 
# Number of pattern initiall found is 70
# final pattern survived is 3
#-- pattern Number List --
# 1 1 Survived
# 7 7 Survived
# 8 8 Survived

このファイルからラベリングされたところの番号を指定してその部分を取り出しpbm化するプログラムもあります.pxm2pbm.javaです.これはソースを公開しています.ソースをとってきてコンパイルして使ってください.

> cp ~fukunaga/ipwork/pxm2pbm.java ./

> javac pxm2pbm.java

そして以下に

> java pxm2pbm twovaluesimple.pxm 1 > twovaluesimple1.pbm

> java pxm2pbm twovaluesimple.pxm 7 > twovaluesimple7.pbm

> java pxm2pbm twovaluesimple.pxm 8 > twovaluesimple8.pbm

として作成したものをそれぞれ掲げます.

課題5:

上記のラベリングは8連結な画素グループを1まとめにするラベリング手法だった.それでは自分の画素と上下左右の4連結のみでつながったグループをひとまとめとするラベリングプログラムppm4cLabeling.javaを作成してください.実際に適用してください.

8連結と4連結とは以下の図のように画素間の領域の連続性の定義の違いを意味します.現行画素の周りのいずれかの画素が含まれている場合にそれを自分の領域と考える場合を8連結、上下左右のつながり画素のみで考える場合を4連結と呼びます.

ヒント:ラベルマスクの簡易化で対応できます.以下の図を二値化処理したあと、8連結と4連結での結果に差が出るかどうか、分割されたグループごとに独立したpbmファイルを作り結果を示してください.

この元ファイルは~fukunaga/ipwork/histkadai5.pgmとしてありますので、これを自分の作業ディレクトリにコピーして作業してください.

階調均一化(イコライジング)

再び以下の画像を見てください.~fukunaga/tmuinthouse.pgm

そしてこの画像のヒストグラムをとるのですが、今度は単純なヒストグラムではなく、ヒストグラムの各チャンネル(ビンともいう)のデータはその階調の頻度ではなくそれまでの頻度の合計をとるようにします.iチャンネルの頻度をy(i)とすると、この頻度の合計はSum(y(k),k≦i)ということになります. そうすると最後のチャンネルはヒストグラムへのデータ総エントリ数となります.上の画像のそのようなヒストグラム(これを累積(cumulated)ヒストグラムと呼びます、赤で表示)と通常のヒストグラム(緑で表示、データ参照軸はY右軸)を以下に示します.

この図にはほかの情報も入っています.ブルーの直線はy=(S/L)x+S/L(Sは画素総数(例えば480×360=172800、Lは最高輝度(例えば255))です.この線はもし画像の輝度階調が一様分布(度数はS/L)であれば、積算分布がこう予想されるという指標となるものです.したがって上の積算ヒストグラムのように大きく波打つ分布の輝度を調整してやれば(図の指し示すようにx→x')一様階調分布の画像に変更可能です.オリジナルのxとそのときの累積度数をC(x)としてやればC(x)=(S/L)(x'+1)からx'=C(x)L/S-1で階調均一化変換がなされます.この操作でコントラストが悪かったり、明るさが偏っている画像の全体的なバランスを改善することが可能になります.

プログラムは

以下のステップで進みます.

  1. 全画像データの読み込み、S(幅×高さ),L(輝度最大値)の取得
  2. 累積ヒストグラムの作成(C(x))
  3. 全画像データの変換、x'=C(x)L/S-1
  4. 全新画像データの書き出し

このプログラムをppmEqualize.javaとします.それを以下に示します.プログラムは~fukunaga/ipwork/ppmImagEqualize.javaです.

> cp ~fukunaga/ipwork/ppmEqualize.java ./

> javac ppmEqualize.java

//import ppmread.class ;
class ppmEqualize {
    public static void main(String args[]) {
        int i,j,k,S,L ;
        ppmread image = new ppmread() ;
        int histdata[],cumhistdata[] ;
        image.ppmmain(args[0]) ;
        // PGM Header output 
        System.out.println("P"+image.magicidno) ;
        System.out.print(image.width+" "+image.height) ;
        if(image.magicidno >1 ) System.out.println(" "+image.maxval) ;
        System.out.println("# Data created by ppmEqualize") ;
        System.out.println("# input filename = "+ image.sfilename ) ;
        System.out.println("# Width    = "+image.width) ;
        System.out.println("# Height   = "+image.height) ;
        if(image.magicidno>1) System.out.println("# Maxval   = "+image.maxval) ;
        L=image.maxval+1 ;
        S=image.width*image.height ;
        histdata=new int [image.maxval+1] ;
        cumhistdata=new int [image.maxval+1] ;
        // Histogram
        for(i=0;i<image.height;i++) 
            for(j=0;j<image.width;j++) 
                histdata[(int)image.gdata[j][i]] += 1;
        // Cumulated Histogram
        for(i=0;i<image.maxval+1;i++) {
            if(i==0) cumhistdata[i]=histdata[i] ;
            else 

                cumhistdata[i] = histdata[i]+cumhistdata[i-1] ;
            //System.err.println("( "+i+") "+histdata[i]+" "+cumhistdata[i]) ;
        }
        int ndataMaxLine=50 ;
        int rdataline,rdatalast ;
        // Conversion and Output
        rdataline = image.width/ndataMaxLine ;
        rdatalast = image.width%ndataMaxLine ;
        for(k=0;k<image.height;k++) {
            for(i=0;i<rdataline;i++) {
                for(j=0;j<ndataMaxLine;j++) {
                    int ij=i*ndataMaxLine+j ;
                    int x=image.gdata[ij][k] ;
                    x=Math.round(cumhistdata[x]*L/(float)S)-1 ;
                    if(x<0) x=0 ;
                    if(x>255) x=255 ;
                    System.out.print(x+" ") ;
                }
                System.out.println() ;
            }
            for(j=0;j<rdatalast;j++) {
                int ij=rdataline*ndataMaxLine+j ;
                int x=image.gdata[ij][k] ;
                x=Math.round(cumhistdata[x]*L/(float)S)-1 ;
                if(x<0) x=0 ;
                if(x>255) x=255 ;
                System.out.print(x+" ") ;
            }
            System.out.println() ;
        }
    }
}

> java ppmEqualize tmuinthouse.pgm > equaltmuinthouse.pgm

とした場合の変換後の画像、およびヒストグラムを以下に示します.