Android(Java)で、JANコード(一次元バーコード)の読み込み!!

古の時代より存在する、商品を区別するためのコード。
バーコード。
日本では、1次元のバーコードの種類として、EAN8,13のような、8桁のバーコード(タバコや塩など)13桁のバーコードが、流通する商品に付属する、主なバーコードの種類で、昨今のPOSレジなどでは、それらのバーコードを読み取れば、商品の値段などが表示されるし、それらの読み取りは「赤外線」が利用されている。
一方、Web全盛期時代+携帯電話全盛期の時代、2次元のバーコードとして、QRコードが使われるようになる。QRコードは、これまでの1次元バーコードと違い、多くの情報(数値以外)を扱えるようにしたもので、また、携帯電話など、カメラが標準搭載されていることから、カメラからの情報でも、正しく読み込めるような仕組みを持つものである。
要は、1次元バーコードは、2次元バーコードと違い、カメラで撮影した情報から、読み取るのは苦手だと言うこと。逆に2次元バーコードは、カメラに適したバーコードである。
それを知らなかった私は、今回Androidからカメラでライブラリ経由でバーコードを読んだときに、殆ど読めなかったことがあったので、それを色々と調べると、納得した次第であり、知らない人もいるだろうからと、とりあえず前置きを書いた次第である。
・・・・・・・・・・・・・
今回Androidで、普通の商品についているバーコード情報を読み取りたいと、そんな話があったので、チャらーと、「Andrid バーコード 読み取り」でググってみたら、Google謹製のZxingなるものがひっかかり、そして「多くのAndroidバーコード読み取りアプリでは、Zxingが利用されているキリッ」と書かれていたで、これを元に作成してみる。
まず、それよりも前に引っかかったのは、Android付属のカメラ。
そもそも、カメラを使ったAndroidアプリなんぞ1度も作ったことないので、色々と調べてみるが、あまりしっくりとこないが、とりあえず、色々と紆余曲折したら、何とかできた。
次に、このZxingを組み込んでみて、いざ1次元バーコードの読み取りを・・・・全然読めない。
色々と参照していたサイトには、Nexus7のカメラは性能悪いから、バーコードの読み取り精度が悪いのは当然キリッと書いてあるので、しかしながら、あまりにも「悪すぎ」で、これでは使い物にならない。
そもそも、「Android+バーコード」で調べると、とにもかくにも「QRコード」がらみ、それ以外が出てこない。たしかに、ZxingでQRコードを読むと、ちゃんと読めたので、Zxing=QRコードなのだろか?
で、色々調べてみると、「1次元バーコードは、そもそもカメラで読むように作られていないキリッ」とか色々書いてあるので、仕方が無い、あきらめるかって思ったのだが、別のページを見ていると、「ZBar」が云々と書いてある。
それで、またもや「ZBar」をググって見ると、これはC、C++で作られたものだけど、NDK経由で一応Androidで使えるとある。
しかしながら、微妙なのが、バージョン。最新バージョンで0.10、Android版にいたっては0.2と、バージョン1ですらない。
まあ、駄目もとってことで、このZxingの変わりに、ZBarを組み込む。
で、実行してみたら、あらびっくり。Zxingと比べると、月とすっぽんぽん(笑)、全然読めるじゃないの。
そもそも、Zxingは、

・1次元バーコードの読み取りは、方向固定。

と言う、まあ言ってみれば、Zxingの存在は、QRコードを読むのが主なようで、それ以外は「蛇足」のようなものらしいってこと。
・・・・・・・・・・・・・・・・・・・・
ただし、ZBarには、重大な欠点がある。それは何かと言えば、参考とする日本語のサイトが少なすぎるってこと。
多分バージョンの低さと、Zxing=Google謹製と言うこともあり、日本での利用数はそれ程多くないのだろう。
なので、サンプルを元に作ってみたら、1次元のバーコード読み取りは、素晴らしく読めるが、これ以上のことを行おうとした場合、

・ソースを読む。
・英語サイトを読む。

以外に方法は無い。
そんなわけで、ソースコードを見てみようって思ったが、Androidのライブラリにはソースコードは付属していない。なので、とりあえず逆アセンブルツールで、zbar.jarを逆アセンブル
今回やりたかったことは、以下の2点。

1.カメラ画像の読み取り範囲を制限する。
2.読み込み対象のフォーマットをEAN13,EAN8に限定する

まず読み取り範囲の制限を調べてみる。

Image.java
public native void setCrop(int paramInt1, int paramInt2, int paramInt3, int paramInt4);

これが、くさいなあってことで、ググって見ると、やはりその通り。
こんな感じで実装すればOK

    private int rectLeft ;
    private int rectTop ;
    private int rectWidth ;
    private int rectHeight ;
    private Point previewSize ;
    @Override
    public void onPreviewFrame( byte[] data, Camera camera ) {
        
        // バーコード画像の生成.
        Image barcode = new Image( previewSize.x,previewSize.y,"Y800" ) ;
        barcode.setData( data ) ;
        
        // 読み取り範囲の設定.
        barcode.setCrop( rectLeft,rectTop,rectWidth,rectHeight ) ;
        
        // バーコード解析.
        int result = scanner.scanImage( barcode ) ;
        
        String text = null ;
        if( result != 0 ) {
            for (Symbol sym : syms) {
                text = sym.getData() ;
                sym.destroy() ;
                break ;
            }
        }
        ・・・・・・・・・・・・・・・・
    }

これで、カメラ画像に対して、範囲指定した部分だけを対象とできる。
次に、読み取り可能なフォーマットを限定する方法。
これが怪しいと思ったので、色々と調べてみる。

ImageScanner.java
  public native void setConfig(int paramInt1, int paramInt2, int paramInt3)
    throws IllegalArgumentException;

でもどう指定していいのかわからないので、さらに調べてみると、

ImageScanner is = new ImageScanner() ;
is.setConfig( Symbol.NONE,Config.ENABLE,0 ) ;

とすることで、特定のフォーマット設定ができるようになり、

is.setConfig( Symbol.EAN13,Config.ENABLE,1 ) ;
is.setConfig( Symbol.EAN8,Config.ENABLE,1 ) ;

とすることで、特定のフォーマットだけを読み取り対象とできる。
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
これで、めでたく、やりたいことが実現できた・・・・って思ったが、これらの設定をしても、しなくてもZbar自身優秀なのか、バーコードの読み取り精度が上がらない。
やはりNexus7のカメラのしょぼさなのか?って思っていたけど、色々と調べてみると、Zbar+Androidで調べると、多くの場合、OpenCVなるものを利用している例が多い。
これは何か?っておもって調べてみたら、インテルが作ってOpenSoruceにしている、画像関連のライブラリで、これを使って、バーコード読み取りした方が、効率があがるか?って思って調べてみると、ライブラリがでか過ぎる、さらに最新版では、OpenCVManagerと言うアプリが必須で、導入をあきらめた。
しかしながら、ZBarを使ったOpenCVで変換する画像情報は、画像を2色に変換しているだけのようなので、これなら自前で作ろうってことで、作ってみるが、まったくうまくいかない。
その前に、Androidのカメラからの画像情報は「YUV」と言うフォーマット、厳密に言えばYUV480SPと言うらしいが、これを

Image barcode = new Image( previewSize.x,previewSize.y,"Y800" ) ;

と言う感じで、"Y800"で変換しているので、実装方法が異なる。

// 画像をRGB4でセット.
Image barcode = new Image( rectWidth,rectHeight,"RGB4" ) ;
barcode.setData( grayBitmapBuffer ) ;

// バーコード解析.
int result = scanner.scanImage( barcode.convert("Y800") ) ;

実際には、こんな感じで「grayBitmapBuffer」はYUVから変換した2値化した、ビットマップ情報(ARGB=8888で、int配列)、これをZBarのイメージにセットする場合は、"Y800"ではなく"RGB4"でセットして、バーコード解析のときに、"Y800"でコンバートした情報を渡せばOK。
とこんな感じにしていなかったので、うまくバーコード認識が行われなかったわけ。
・・・・・・・・・・・・・・・・・・・・・・
単純に2値化した情報変換し、ARGB形式にしたものを読み込むが、まあそれをしてもしなくても、この変換をしない場合と比べても、あまり読み込み制度に変わりが無い。
大津二値化方式で変換しても、それ程たいして変わらない。
そもそも、バーコードで線の太さで数値を表しているわけで、2値化した画像をみるとわかるが、線の太さが微妙にマバラで、黒のバーコード線で背景白ならば、それ程でもないけど、バーコード線が青色とかのっ場合、カメラが悪いのか、ノイズがのっかり、マバラな率が高い。
なので、画像変換のアルゴリズムから調べてみようってことで、色々見ていると「メディアンフィルタ」と言う方法があり、これは単純で、ある一定のWidthのドット範囲色から、一番真ん中の値(平均ではなく、範囲分を取得してそれをソートして、その真ん中)に塗り替える、これによりノイズを減らすことができる。
しかし、一々配列作ってソート処理するのも、速度が遅いし、そもそもほしい情報は、線を正しく表示させたいだけなので、それならば、その範囲内のうち小さい値(黒は0なので)を採用すればOKだろうから、じゃあ、とりあえずってことで、以下のように作ってみた。

    /** MEDIAN長. **/
    private static final int MEDIAN_LENGTH = 15 ;
    private static final int MEDIAN_HALF = MEDIAN_LENGTH >> 1 ;
    private static final int MEDIAN_HALF_P1 = MEDIAN_HALF + 1 ;
    
    /** YUVデータを、グレイスケール+メディアンフィルタ改+2値化でビットマップ変換. **/
    private static final void convetGrayScaleBitmap( int[] out,byte[] yuvData,
        int dataWidth,int dataHeight,int left,int top,int width,int height ) {
        
        int w = width ;
        int h = height ;
        int inputOffset,outputOffset,y,x,i,n,t,c ;
        final byte[] yuv = yuvData;
        
        // 中心位置の最大最小値の平均値を取得.
        int hc = 0 ;
        {
            int hn = 255 ;
            int hm = 0 ;
            int hLen = ( h >> 1 ) + ( h >> 2 ) ;
            inputOffset = ( ( top * dataWidth ) + left ) +
                ( ( h >> 2 ) * dataWidth ) + ( w >> 1 ) ;
            for ( y = ( h >> 2 ) ; y < hLen ; y ++ ) {
                n = yuv[ inputOffset ] & 0xff ;
                if( hn > n ) {
                    hn = n ;
                }
                if( hm < n ) {
                    hm = n ;
                }
                inputOffset += dataWidth ;
            }
            hc = ( hn + hm ) >> 1 ;
        }
        
        // グレースケール処理+メディアンフィルタ改+2値化.
        inputOffset = ( top * dataWidth ) + left ;
        for( y = 0 ; y < h ; y ++ ) {
            
            // Y中心の平均値を取得.
            outputOffset = y * w;
            for ( x = 0 ; x < w ; x ++ ) {
                
                // 左端の処理の場合.
                if( x <= MEDIAN_HALF ) {
                    n = inputOffset ;
                }
                // 右端の場合.
                else if( x + MEDIAN_HALF_P1 > w ) {
                    n = inputOffset + ( x - MEDIAN_LENGTH ) ;
                }
                // 端以外の処理の場合.
                else {
                    n = inputOffset + ( x - MEDIAN_HALF ) ;
                }
                
                // 黒属性を対象とする.
                t = yuv[ n ] & 0xff ;
                for( i = 1 ; i < MEDIAN_LENGTH ; i ++ ) {
                    if( t > ( c = yuv[ n + i ] & 0xff ) ) {
                        t = c ;
                    }
                }
                
                // 最大最小の平均値で、2値化.
                if( hc < t ) {
                    out[ outputOffset + x ] = 0xFFffffff ;
                }
                else {
                    out[ outputOffset + x ] = 0xFF000000 ;
                }
                //out[ outputOffset + x ] = 0xFF000000 | ( t * 0x00010101 ) ;
            }
            inputOffset += dataWidth ;
        }
    }

とりあえず、画像補正した結果なのか、前よりかは、バーコードの読み取り精度が上がった。
・・・・・・・・・・・・・・・・・・・・・・・
最後に、実装したソースの一部分を乗せる。
全部乗せると、でか過ぎるので、Zbarの必要な部分だけを載せる。

    /** ビットマップ用バッファ. **/
    private int[] grayBitmapBuffer ;  // この配列は領域確定時に作っておく。
    
    // バーコードの読み込みは2回チェック.
    private String _befCode = null ;

    /** カメラから、バーコード認識を行う処理. **/
    @Override
    public void onPreviewFrame( byte[] data, Camera camera ) {
        
        // グレイスケールBitmapを生成.
        convetGrayScaleBitmap( grayBitmapBuffer,data,previewSize.x,
            previewSize.y,rectLeft,rectTop,rectWidth,rectHeight ) ;
        
        // 画像をRGB4セット.
        Image barcode = new Image( rectWidth,rectHeight,"RGB4" ) ;
        barcode.setData( grayBitmapBuffer ) ;
        
        // バーコード解析.
        int result = scanner.scanImage( barcode.convert("Y800") ) ;
        
        // Imageデータの破棄.
        barcode.destroy() ;
        
        // 処理結果を取得.
        if (result != 0) {
            
            // 解析結果を取得.
            SymbolSet syms = scanner.getResults();
            
            String c ;
            
            // 取得一覧.
            for (Symbol sym : syms) {

                // 厳密性を保つため、2度チェック.
                c = sym.getData() ;
                if( _befCode == null ) {
                    
                    // 1回目の場合.
                    _befCode = c ;
                }
                // 2回目が一致する場合.
                else if( _befCode.equals( c ) ) {
                    
                    // 解析内容を元にコールバックメソッド処理.
                    callback.execute( c );
                    
                    // この画面処理クローズ.
                    close() ;
                    return ;
                }
                // 一致しない場合は一旦クリア.
                else {
                    
                    _befCode = null ;
                }

            }
        }
    }

上記の_befCode のように、何故このようにしているかと言えば、小さなバーコードの場合、たまに変な値が返却されることがあったので、とりあえず2度チェックして同じ値ならばOKと、簡易チェックをした次第。
また、callback.executeは、バーコード情報が正しく読めた場合に呼び出されるコールバックオブジェクト。closeは、作ったアプリのバーコード認識画面を、ダイアログで作っているから、そのダイアログをクローズするためのオリジナルな処理。
・・・・・・・・・・・・・・・・・・・
まあ、たかがバーコードの読み取り1つで、偉く面倒だし、たったこれだけのことで、5日もかかってしまったわけで、たかがバーコード、されどバーコード。
世間一般で使われているからと言って、決して楽ではないねえって思ったので、役に立てば幸いかと。
 
部分的では結構わかりづらいかもしれないと思ったので、とりあえず、全体のソースを記述。

public final class ReadJANBarcode implements SurfaceHolder.Callback,
    AutoFocusCallback, PreviewCallback {
    
    /** nexus7用のカメラID指定. **/
    protected static final int DEF_CAMERA_ID = ReadBarcode.DEF_CAMERA_ID ;
    
    /** 垂直精度. **/
    protected static final int DENSITY = ReadBarcode.DENSITY ;
    
    /** 特定の解像度以下、以上のカメラは処理対象外. **/
    protected static final int MIN_PREVIEW_PIXELS = ReadBarcode.MIN_PREVIEW_PIXELS ;
    protected static final int MAX_PREVIEW_PIXELS = ReadBarcode.MAX_PREVIEW_PIXELS ;
    
    /** 認識領域のコンテンツ定義. **/
    protected static final int WRAP_CONTENT = ReadBarcode.WRAP_CONTENT ;
    
    /** デフォルトの認識領域カラー. **/
    protected static final int DEF_TARGET_COLOR = ReadBarcode.DEF_TARGET_COLOR ;
    
    /** デフォルトの認識中央線カラー. **/
    protected static final int DEF_TARGET_CENTER_LINE_COLOR = ReadBarcode.DEF_TARGET_CENTER_LINE_COLOR ;
    
    /** デフォルトのフォントカラー. **/
    protected static final int DEF_MESSAGE_FONT_COLOR = ReadBarcode.DEF_MESSAGE_FONT_COLOR ;
    
    /** デフォルトの認識中央線太さ. **/
    protected static final int DEF_TARGET_CENTER_LINE_STROKE = ReadBarcode.DEF_TARGET_CENTER_LINE_STROKE ;
    
    /** 画面に描画する文字のWidthに対する比率. **/
    protected static final int WIDTH_FONT_SIZE = ReadBarcode.WIDTH_FONT_SIZE ;
    
    private SurfaceView surfaceView ;
    private Camera camera ;
    private ImageScanner scanner;
    private boolean useCamera = false ;
    
    private Activity activity ;
    private Object sync ;
    private Dialog dialog ;
    private Point previewSize;
    
    private LinearLayout rectView ;
    private int rectLeft ;
    private int rectTop ;
    private int rectWidth ;
    private int rectHeight ;
    
    private NList targetList ;
    private int cameraId = -1 ;
    private boolean widthFlag ;
    private boolean isMirror ;
    
    private CallbackReadBarcode callback ;
    
    /**
     * コンストラクタ.
     * @param activity 対象のActivityを設定します.
     * @param sync 同期オブジェクトを設定します.
     * @param callback 認識された場合に呼び出されるコールバックオブジェクトを設定します.
     */
    public ReadJANBarcode( Activity activity,Object sync,CallbackReadBarcode callback ) {
        this( activity,sync,-1,-1,null,-1,callback ) ;
    }
    
    /**
     * コンストラクタ.
     * @param activity 対象のActivityを設定します.
     * @param sync 同期オブジェクトを設定します.
     * @param target 読み込み対象のタイプ群を設定します.
     *               [null]の場合は、全読み込みとなります.
     * @param callback 認識された場合に呼び出されるコールバックオブジェクトを設定します.
     */
    public ReadJANBarcode( Activity activity,Object sync,int[] target,CallbackReadBarcode callback ) {
        this( activity,sync,-1,-1,target,-1,callback ) ;
    }
    
    /**
     * コンストラクタ.
     * @param activity 対象のActivityを設定します.
     * @param sync 同期オブジェクトを設定します.
     * @param width 認識区域のWidth値を設定します.
     *              認識区域を無くす場合は-1を設定します.
     * @param height 認識区域のHeight値を設定します.
     *              認識区域を無くす場合は-1を設定します.
     * @param callback 認識された場合に呼び出されるコールバックオブジェクトを設定します.
     */
    public ReadJANBarcode( Activity activity,Object sync,int width,int height,CallbackReadBarcode callback ) {
        this( activity,sync,width,height,null,-1,callback ) ;
    }
    
    /**
     * コンストラクタ.
     * @param activity 対象のActivityを設定します.
     * @param sync 同期オブジェクトを設定します.
     * @param width 認識区域のWidth値を設定します.
     *              認識区域を無くす場合は-1を設定します.
     * @param height 認識区域のHeight値を設定します.
     *              認識区域を無くす場合は-1を設定します.
     * @param color 認識領域のカラーを設定します.
     *               [-1]を設定した場合は、デフォルトのカラーが定義されます.
     * @param callback 認識された場合に呼び出されるコールバックオブジェクトを設定します.
     */
    public ReadJANBarcode( Activity activity,Object sync,int width,int height,int color,CallbackReadBarcode callback ) {
        this( activity,sync,width,height,null,color,callback ) ;
    }
    
    /**
     * コンストラクタ.
     * @param activity 対象のActivityを設定します.
     * @param sync 同期オブジェクトを設定します.
     * @param width 認識区域のWidth値を設定します.
     *              認識区域を無くす場合は-1を設定します.
     * @param height 認識区域のHeight値を設定します.
     *              認識区域を無くす場合は-1を設定します.
     * @param target 読み込み対象のタイプ群を設定します.
     *               [null]の場合は、全読み込みとなります.
     * @param callback 認識された場合に呼び出されるコールバックオブジェクトを設定します.
     */
    public ReadJANBarcode( Activity activity,Object sync,int width,int height,int[] target,CallbackReadBarcode callback ) {
        this( activity,sync,width,height,target,-1,callback ) ;
    }
    
    /**
     * コンストラクタ.
     * @param activity 対象のActivityを設定します.
     * @param sync 同期オブジェクトを設定します.
     * @param width 認識区域のWidth値を設定します.
     *              認識区域を無くす場合は-1を設定します.
     * @param height 認識区域のHeight値を設定します.
     *              認識区域を無くす場合は-1を設定します.
     * @param target 読み込み対象のタイプ群を設定します.
     *               [null]の場合は、全読み込みとなります.
     * @param color 認識領域のカラーを設定します.
     *               [-1]を設定した場合は、デフォルトのカラーが定義されます.
     * @param callback 認識された場合に呼び出されるコールバックオブジェクトを設定します.
     */
    public ReadJANBarcode( Activity activity,Object sync,int width,int height,int[] target,int color,CallbackReadBarcode callback ) {
        
        this.activity = activity ;
        this.sync = sync ;
        surfaceView = new SurfaceView( activity ) ;
        SurfaceHolder holder = surfaceView.getHolder() ;
        holder.addCallback( this ) ;
        
        // 現在の画面サイズを取得.
        DisplayMetrics metrics = activity.getResources().getDisplayMetrics() ;
        
        // カメラ表示用ダイアログ作成.
        Dialog d = new Dialog( activity ) {
            @Override
            public boolean dispatchTouchEvent(MotionEvent ev) { 
                close() ;
                return true ;
            }
        } ;
        d.setCancelable( true ) ;
        d.setCanceledOnTouchOutside( true ) ;
        
        // タイトルなし.
        d.requestWindowFeature(Window.FEATURE_NO_TITLE) ;
        
        // キャンセル処理.
        d.setOnCancelListener( new DialogInterface.OnCancelListener() {
            
            // キャンセル処理.
            @Override
            public void onCancel(DialogInterface dialog) {
                _close() ;
            }
        }) ;
        
        // カメラ表示用ダイアログの設定.
        WindowManager.LayoutParams dv = d.getWindow().getAttributes() ;
        dv.width = metrics.widthPixels ;
        dv.height = metrics.heightPixels ;
        dv.gravity = Gravity.CENTER | Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL ;
        d.getWindow().setAttributes( dv ) ;
        
        // フレームレイヤーを作成.
        FrameLayout frame = new FrameLayout( activity ) ;
        FrameLayout.LayoutParams fv = new FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.MATCH_PARENT,FrameLayout.LayoutParams.MATCH_PARENT ) ;
        frame.setLayoutParams( fv ) ;
        d.setContentView( frame ) ;
        
        frame.addView( surfaceView );
        
        // 認識区域を設定.
        LinearLayout v = null ;
        if( width != -1 && height != -1 ) {
            
            v = new LinearLayout( activity ) ;
            FrameLayout.LayoutParams tv = new FrameLayout.LayoutParams( WRAP_CONTENT, WRAP_CONTENT ) ;
            tv.gravity = Gravity.CENTER | Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL ;
            tv.width = width ;
            tv.height = height ;
            v.setLayoutParams( tv ) ;
            if( color == -1 ) {
                color = DEF_TARGET_COLOR ;
            }
            v.setBackgroundColor( color ) ;
            
            // 真ん中に線を引く.
            v.addView( new LineView( activity,width,height,
                DEF_TARGET_CENTER_LINE_COLOR,DEF_TARGET_CENTER_LINE_STROKE ) ) ;
            
            // 利用説明を描画.
            TextView text = new TextView( activity ) ;
            text.setText( "\n枠内の線に水平状態で\nバーコードを写してください\n" +
                "(画面タッチでキャンセル)" ) ;
            text.setTypeface(Typeface.DEFAULT_BOLD ) ;
            text.setTextSize( (int)( metrics.widthPixels / WIDTH_FONT_SIZE ) );
            
            text.setTextColor( DEF_MESSAGE_FONT_COLOR ) ;
            text.setGravity( Gravity.TOP | Gravity.CENTER ) ;
            
            frame.addView( v ) ;
            frame.addView( text ) ;
            
        }
        
        // zbarを生成.
        ImageScanner sc = new ImageScanner() ;
        sc.setConfig( 0, Config.X_DENSITY, DENSITY ) ;
        sc.setConfig( 0, Config.Y_DENSITY, DENSITY ) ;
        sc.enableCache( false ) ;
        
        // 読み込みタイプが指定されている場合は、その条件に従う.
        NList tList = null ;
        if( target != null && target.length > 0 ) {
            sc.setConfig( Symbol.NONE,Config.ENABLE,0 ) ;
            int len = target.length ;
            for( int i = 0 ; i < len ; i ++ ) {
                sc.setConfig( target[ i ],Config.ENABLE,1 ) ;
            }
            tList = new NList( target ) ;
            tList.sort() ;
        }
        
        this.dialog = d ;
        this.rectView = v ;
        this.callback = callback ;
        this.scanner = sc ;
        this.targetList = tList ;
        
        d.show();
        useCamera = true ;
        
    }
    
    /** 内部クローズ. **/
    protected final void _close() {
        synchronized( sync ) {
            useCamera = false ;
            if( camera != null ) {
                try {
                    camera.cancelAutoFocus();
                } catch( Exception e ) {}
                try {
                    camera.setPreviewCallback(null);
                } catch( Exception e ) {}
                try {
                    camera.stopPreview();
                } catch( Exception e ) {}
                try {
                    camera.release() ;
                } catch( Exception e ) {}
            }
            if( scanner != null ) {
                try {
                    scanner.destroy() ;
                } catch( Exception e ) {
                }
            }
            
            scanner = null ;
            activity = null ;
            surfaceView = null ;
            camera = null ;
        }
    }
    
    /** カメラクローズ. **/
    public final void close() {
        synchronized( sync ) {
            if( useCamera ) {
                if( dialog != null ) {
                    dialog.cancel() ;
                }
                _close() ;
            }
            dialog = null ;
        }
    }
    
    /** 情報が有効かチェック. **/
    public final boolean isBarcode() {
        synchronized( sync ) {
            return useCamera ;
        }
    }
    
    // surface新規作成時の呼び出し.
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if( !isBarcode() ) {
            return ;
        }
        
        synchronized( sync ) {
            
            cameraId = -1 ;
            
            // CameraInfoからバックフロントカメラのidを取得
            int len = Camera.getNumberOfCameras() ;
            Camera.CameraInfo info = new Camera.CameraInfo();
            for( int i = 0 ; i < len ; i ++ ) {
                Camera.getCameraInfo( i,info ) ;
                if( info.facing == Camera.CameraInfo.CAMERA_FACING_BACK ){
                    cameraId = i;
                    break ;
                }
            }
            // バックフロントのカメラが存在しない場合は、デフォルトのカメラIDをセット.
            if( cameraId == -1 ) {
                cameraId = DEF_CAMERA_ID ;
            }
            
            // カメラOpen.
            try {
                camera = Camera.open( cameraId );
            } catch( Exception e ) {
            }
            try {
                camera.setPreviewDisplay(holder);
            } catch (IOException e) {
            }
            
            // 対象カメラの向きを取得.
            Camera.getCameraInfo( cameraId,info ) ;
            isMirror = (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) ;
            
            // カメラ描画開始.
            setPreviewSize() ;
        }
    }
    
    // surface変更時の呼び出し.
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        if( !isBarcode() ) {
            return ;
        }
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewSize(previewSize.x, previewSize.y);
        camera.setParameters(parameters);
        camera.startPreview();
        
        // 画面回転補正。
        if (getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
            camera.setDisplayOrientation(90);
        } else {
            camera.setDisplayOrientation(0);
        }
        
        // オートフォーカス設定。
        camera.autoFocus( this ) ;
    }
    
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if( !isBarcode() ) {
            return ;
        }
        camera.stopPreview();
    }
    
    private void setPreviewSize() {
        if( !isBarcode() ) {
            return ;
        }
        
        // カメラ解像度リストを取得.
        Camera.Parameters parameters = camera.getParameters();
        List<Size> rawPreviewSizes = parameters.getSupportedPreviewSizes();
        List<Size> supportPreviewSizes = new ArrayList<Size>(rawPreviewSizes);
        Collections.sort(supportPreviewSizes, new Comparator<Size>() {
            @Override
            public int compare(Size lSize, Size rSize) {
                int lPixels = lSize.width * lSize.height;
                int rPixels = rSize.width * rSize.height;
                if (rPixels < lPixels) {
                    return -1;
                }
                else if (rPixels > lPixels) {
                    return 1;
                }
                return 0;
            }
        });
        
        int supportWidth,supportHeight ;
        Size supportPreviewSize = null ;
        int len = supportPreviewSizes.size() ;
        // 解像度の高い順.
        for( int i = 0 ; i < len ; i ++ ) {
            supportPreviewSize = supportPreviewSizes.get( i ) ;
            supportWidth = supportPreviewSize.width;
            supportHeight = supportPreviewSize.height;
            int pixels = supportWidth * supportHeight;
            if (pixels < MIN_PREVIEW_PIXELS || pixels > MAX_PREVIEW_PIXELS) {
                continue;
            }
            break ;
        }
        // 存在しない場合は、最初の条件を対象とする.
        if( supportPreviewSize == null ) {
            supportPreviewSize = supportPreviewSizes.get( 0 ) ;
        }
        
        // カメラサイズを取得.
        previewSize = new Point( supportPreviewSize.width,supportPreviewSize.height ) ;
        
        // 認識区域に対する条件を設定.
        DisplayMetrics metrics = activity.getResources().getDisplayMetrics() ;
        int screenWidth = metrics.widthPixels ;
        int screenHeight = metrics.heightPixels ;
        if( screenWidth < screenHeight ) {
            
            // 縦向き.
            widthFlag = true ;
            int n = screenWidth ;
            screenWidth = screenHeight ;
            screenHeight = n ;
        }
        else {
            
            // 横向き.
            widthFlag = false ;
        }
        
        // 画面とカメラの比率を計算.
        float previewWidthRatio = (float) previewSize.x / (float) screenWidth;
        float previewHeightRatio = (float) previewSize.y / (float) screenHeight;
        
        // 認識領域が存在する場合は、範囲を取得.
        if( rectView != null ) {
            
            // 縦向きの場合.
            if( widthFlag ) {
                rectLeft = (int) (rectView.getTop() * previewWidthRatio) ;
                rectTop = (int) (rectView.getLeft() * previewHeightRatio) ;
                rectWidth = (int) (rectView.getHeight() * previewWidthRatio) ;
                rectHeight = (int) (rectView.getWidth() * previewHeightRatio) ;
            }
            // 横向きの場合.
            else {
                rectLeft = (int) (rectView.getLeft() * previewWidthRatio) ;
                rectTop = (int) (rectView.getTop() * previewHeightRatio) ;
                rectWidth = (int) (rectView.getWidth() * previewWidthRatio) ;
                rectHeight = (int) (rectView.getHeight() * previewHeightRatio) ;
            }
        }
        // 認識領域が存在しない場合は、全体を対象とする.
        else {
            rectLeft = 0 ;
            rectTop = 0 ;
            rectWidth = previewSize.x ;
            rectHeight = previewSize.y ;
        }
        
        // ビットマップ用バッファの作成.
        grayBitmapBuffer = new int[ rectWidth * rectHeight ] ;
    }
    
    /** ビットマップ用バッファ. **/
    private int[] grayBitmapBuffer ;
    
    // バーコードの読み込みは2回チェック.
    private String _befCode = null ;
    
    /** カメラから、バーコード認識を行う処理. **/
    @Override
    public void onPreviewFrame( byte[] data, Camera camera ) {
        if( !isBarcode() ) {
            return ;
        }
        
        // 前面カメラの場合は、バイナリ情報を左右変換.
        if( isMirror ) {
            reverseHorizontal( data,previewSize.x,previewSize.y,
                rectLeft,rectTop,rectWidth,rectHeight ) ;
        }
        
        // グレイスケールBitmapを生成.
        convetGrayScaleBitmap( grayBitmapBuffer,data,previewSize.x,previewSize.y,
            rectLeft,rectTop,rectWidth,rectHeight ) ;
        
        // 画像をRGB4設定.
        Image barcode = new Image( rectWidth,rectHeight,"RGB4" ) ;
        barcode.setData( grayBitmapBuffer ) ;
        
        // バーコード解析.
        int result = scanner.scanImage( barcode.convert("Y800") ) ;
        
        // Imageデータの破棄.
        barcode.destroy() ;
        
        // 処理結果を取得.
        if (result != 0) {
            
            // 解析結果を取得.
            SymbolSet syms = scanner.getResults();
            
            // 読み込みフォーマット定義がされている場合.
            if( targetList != null ) {
                
                String c ;
                
                // チェックする.
                for (Symbol sym : syms) {
                    
                    // 取得した条件がフォーマットと一致する場合.
                    if( targetList.search( sym.getType() ) != -1 ) {
                        
                        // 厳密性を保つため、2度チェック.
                        c = sym.getData() ;
                        if( _befCode == null ) {
                            
                            // 1回目の場合.
                            _befCode = c ;
                        }
                        // 2回目が一致する場合.
                        else if( _befCode.equals( c ) ) {
                            
                            // 解析内容を元にコールバックメソッド処理.
                            callback.execute( c );
                            
                            // この画面処理クローズ.
                            close() ;
                            return ;
                        }
                        // 一致しない場合は一旦クリア.
                        else {
                            
                            _befCode = null ;
                        }
                    }
                }
            }
            // 読み込みフォーマット定義がされていない場合.
            else {
                
                for (Symbol sym : syms) {
                    
                    // 解析内容を元にコールバックメソッド処理.
                    callback.execute( sym.getData() );
                    
                    // この画面処理クローズ.
                    close() ;
                    return ;
                }
            }
        }
        
        // オートフォーカス設定.
        camera.autoFocus( this ) ;
    }
    
    /** 中央にLineを設定. **/
    @SuppressLint("DrawAllocation")
    private final class LineView extends View {
        int width ;
        int height ;
        int color ;
        int stroke ;
        
        LineView( Context context,int width,int height,int color,int stroke ) {
            super( context ) ;
            this.width = width ;
            this.height = height ;
            this.color = color ;
            this.stroke = stroke ;
        }
        @Override
        protected final void onDraw( Canvas canvas ) {
            super.onDraw( canvas ) ;
            Paint paint = new Paint();
            paint.setColor( color ) ;
            paint.setStrokeWidth( stroke ) ;
            
            canvas.drawLine(0, height>>1, width, height>>1, paint);
            canvas.drawLine(width>>1, 0, width>>1, height, paint);
        }
    }
    
    /** 前面カメラの撮影の場合は、バイナリのWidth方向を反転させる. **/
    private static final byte[] reverseHorizontal( byte[] yuvData,
        int dataWidth,int dataHeight,int left,int top,int width,int height ) {
        byte temp ;
        int middle,rowStart,x1,x2,y ;
        for (y = 0, rowStart = top * dataWidth + left; y < height; y++, rowStart += dataWidth) {
            middle = rowStart + width >> 1;
            for (x1 = rowStart, x2 = rowStart + width - 1; x1 < middle; x1++, x2--) {
                temp = yuvData[x1];
                yuvData[x1] = yuvData[x2];
                yuvData[x2] = temp;
            }
        }
        return yuvData ;
    }
    
    /** MEDIAN長. **/
    private static final int MEDIAN_LENGTH = 15 ;
    private static final int MEDIAN_HALF = MEDIAN_LENGTH >> 1 ;
    private static final int MEDIAN_HALF_P1 = MEDIAN_HALF + 1 ;
    
    /** YUVデータを、グレイスケール+メディアンフィルタ改+2値化でビットマップ変換. **/
    private static final void convetGrayScaleBitmap( int[] out,byte[] yuvData,
        int dataWidth,int dataHeight,int left,int top,int width,int height ) {
        
        int w = width ;
        int h = height ;
        int inputOffset,outputOffset,y,x,i,n,t,c ;
        final byte[] yuv = yuvData;
        
        // 中心位置の最大最小値の平均値を取得.
        int hc = 0 ;
        {
            int hn = 255 ;
            int hm = 0 ;
            int hLen = ( h >> 1 ) + ( h >> 2 ) ;
            inputOffset = ( ( top * dataWidth ) + left ) +
                ( ( h >> 2 ) * dataWidth ) + ( w >> 1 ) ;
            for ( y = ( h >> 2 ) ; y < hLen ; y ++ ) {
                n = yuv[ inputOffset ] & 0xff ;
                if( hn > n ) {
                    hn = n ;
                }
                if( hm < n ) {
                    hm = n ;
                }
                inputOffset += dataWidth ;
            }
            hc = ( hn + hm ) >> 1 ;
        }
        
        // グレースケール処理+メディアンフィルタ改+2値化.
        inputOffset = ( top * dataWidth ) + left ;
        for( y = 0 ; y < h ; y ++ ) {
            
            // Y中心の平均値を取得.
            outputOffset = y * w;
            for ( x = 0 ; x < w ; x ++ ) {
                
                // 左端の処理の場合.
                if( x <= MEDIAN_HALF ) {
                    n = inputOffset ;
                }
                // 右端の場合.
                else if( x + MEDIAN_HALF_P1 > w ) {
                    n = inputOffset + ( x - MEDIAN_LENGTH ) ;
                }
                // 端以外の処理の場合.
                else {
                    n = inputOffset + ( x - MEDIAN_HALF ) ;
                }
                
                // 黒属性を対象とする.
                t = yuv[ n ] & 0xff ;
                for( i = 1 ; i < MEDIAN_LENGTH ; i ++ ) {
                    if( t > ( c = yuv[ n + i ] & 0xff ) ) {
                        t = c ;
                    }
                }
                
                // 最大最小の平均値で、2値化.
                if( hc < t ) {
                    out[ outputOffset + x ] = 0xFFffffff ;
                }
                else {
                    out[ outputOffset + x ] = 0xFF000000 ;
                }
                //out[ outputOffset + x ] = 0xFF000000 | ( t * 0x00010101 ) ;
            }
            inputOffset += dataWidth ;
        }
    }
    
    /** オートフォーカス開始. **/
    @Override
    public void onAutoFocus(boolean success, Camera camera) {
        if( !isBarcode() ) {
            return ;
        }
        if (success) {
            camera.setOneShotPreviewCallback(this);
        }
    }
    
    /**
     * 現在のカメラIDを取得.
     * @return int 現在のカメラIDが返却されます.
     *             [-1]の場合、選択されていません.
     */
    public int getCameraId() {
        return cameraId ;
    }
    
    /**
     * 前面カメラか取得.
     * @return boolean [true]の場合、前面のカメラです.
     */
    public boolean getFront() {
        return isMirror ;
    }
    
    /** 画面の向きを取得. **/
    private final int getOrientation() {
        
        // 縦方向にレイアウトしたい場合:vertical
        // 横方向にレイアウトしたい場合:horizontal
        return activity.getResources().getConfiguration().orientation;
    }
    
}
Add Star
  • Android初心者

    初めまして、突然申し訳ありません。
    こちらのZBarを使ったAndroidのバーコードリーダーについて拝見させていただきました。
    私もmaachang様のソースを参考に、読取精度を上げたZbarによる1次元バーコードリーダーを実装したいと思っているのですが
    お恥ずかしい話、知識が浅くこれをどうやって実装すれば良いかよくわからず、苦戦しております。

    不躾で申し訳ありませんが、このソースを使ったActivityクラス等のサンプル等はありますでしょうか?

コメントを書く