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; } }