2012年6月27日水曜日

AndroidでQRコードを読み込む(ZXing 2.0)

今日はAndroidをさわってました。
(毎日何をやってるのかだんだんわからなくなってきた。。。)

AndroidでQRコードを使う場合、だいたいがZXing(ゼブラクロッシング)を利用してアプリを構築しているみたいです。
今日はカメラ上からQRコードを読み込んで読み込んだ内容を表示するという単純なものでしたがなかなかすんなりいかず苦労したんでメモ程度に。

とりあえずライブラリをダウンロードします。(現在の最新はZXing-2.0.zip)
http://code.google.com/p/zxing/downloads/list

ダウンロードしたファイルを解凍するといろいろ入ってますがアプリ作るのに必要なのは以下

  • core/core.jar
  • javase/javase.jar
  • android/src/com/google/zxing/client/android/PlanarYUVLuminanceSource.java

まずAndroidのプロジェクトを作成。(ここではZXingTestとしました)
続けて「libs」フォルダと「com.google.zxing.client.android」パッケージを作成します。
libsには「core.jar」と「javase.jar」、パッケージには「PlanarYUVLuminanceSource.java」をコピーします。
libsはこのままでは使えないのでそれぞれ右クリックしてビルド・パスに追加をしておきます。
追加後のツリーはこんなかんじです。



次にメインの画面です。(res/layout/main.xml)
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    
    <SurfaceView
        android:id="@+id/preview_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />
    
    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
        
        <View
            android:id="@+id/center_view"
            android:layout_width="300dip"
            android:layout_height="180dip"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true"
            android:background="#55ff6666" />
        
    </RelativeLayout>
    
</FrameLayout>

SurfaceViewはカメラを使うんでお決まりですかね。
Viewにしてる部分はQRコードを読み込むエリアを描画しています。

次にカメラを使うのでマニフェストファイルを修正します。(AndroidManifest.xml)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.kuseful.zxingtest"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="8" />
    
    <uses-permission android:name="android.permission.CAMERA" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".ZXingTestActivity"
            android:label="@string/app_name"
            android:screenOrientation="landscape" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

パーミッションの追加とあとアプリを横向きとするためscreenOrientationをlandscapeに指定しています。

次にメインのソース部分です。(ZXingTextActivity.java)
package jp.kuseful.zxingtest;

import java.io.IOException;

import com.google.zxing.BinaryBitmap;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.Result;
import com.google.zxing.client.android.PlanarYUVLuminanceSource;
import com.google.zxing.common.HybridBinarizer;

import android.app.Activity;
import android.content.Context;
import android.graphics.Point;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;

public class ZXingTestActivity extends Activity
    implements SurfaceHolder.Callback, Camera.PreviewCallback, Camera.AutoFocusCallback {
    
    private static final String TAG = "ZXingTest";
    
    private static final int MIN_PREVIEW_PIXCELS = 320 * 240;
    private static final int MAX_PREVIEW_PIXCELS = 800 * 480;

    private Camera myCamera;
    private SurfaceView surfaceView;
    
    private Boolean hasSurface;    
    private Boolean initialized;
    
    private Point screenPoint;
    private Point previewPoint;
    
    /** lifecycle */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        Window window = getWindow();
        window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        
        hasSurface = false;
        initialized = false;
        
        setContentView(R.layout.main);
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        
        surfaceView = (SurfaceView)findViewById(R.id.preview_view);
        SurfaceHolder holder = surfaceView.getHolder();
        if (hasSurface) {
            initCamera(holder);
        } else {
            holder.addCallback(this);
            holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        }
    }
    
    @Override
    protected void onPause() {
        closeCamera();
        if (!hasSurface) {
            SurfaceHolder holder = surfaceView.getHolder();
            holder.removeCallback(this);
        }
        super.onPause();
    }

    /** SurfaceHolder.Callback */
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (!hasSurface) {
            hasSurface = true;
            initCamera(holder);
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        hasSurface = false;
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    /** Camera.AutoFocusCallback */
    @Override
    public void onAutoFocus(boolean success, Camera camera) {
        if (success)
            camera.setOneShotPreviewCallback(this);
    }
    
    /** devices */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (myCamera != null) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                Camera.Parameters parameters = myCamera.getParameters();
                if (!parameters.getFocusMode().equals(Camera.Parameters.FOCUS_MODE_FIXED)) {
                    myCamera.autoFocus(this);
                }
            }
        }
        return true;
    }

    /** Camera.PreviewCallback */
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        View centerView = (View)findViewById(R.id.center_view);
        
        int left = centerView.getLeft() * previewPoint.x / screenPoint.x;
        int top = centerView.getTop() * previewPoint.y / screenPoint.y;
        int width = centerView.getWidth() * previewPoint.x / screenPoint.x;
        int height = centerView.getHeight() * previewPoint.y / screenPoint.y;
        
        PlanarYUVLuminanceSource source    = new PlanarYUVLuminanceSource(
                                                    data,
                                                    previewPoint.x,
                                                    previewPoint.y,
                                                    left,
                                                    top,
                                                    width,
                                                    height,
                                                    false);
        
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        MultiFormatReader reader = new MultiFormatReader();
        try {
            Result result = reader.decode(bitmap);
            Toast.makeText(this, result.getText(), Toast.LENGTH_LONG).show();
        } catch (Exception e) {
            Toast.makeText(this, "error: " + e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }
    
    /**
     * カメラ情報を初期化
     * @param holder
     */
    private void initCamera(SurfaceHolder holder) {
        try {
            openCamera(holder);
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    private void openCamera(SurfaceHolder holder) throws IOException {
        if (myCamera == null) {
            myCamera = Camera.open();
            if (myCamera == null) {
                throw new IOException();
            }
        }
        myCamera.setPreviewDisplay(holder);
        
        if (!initialized) {
            initialized = true;
            initFromCameraParameters(myCamera);
        }
        
        setCameraParameters(myCamera);
        myCamera.startPreview();
    }
    
    /**
     * カメラ情報を破棄
     */
    private void closeCamera() {
        if (myCamera != null) {
            myCamera.stopPreview();
            myCamera.release();
            myCamera = null;
        }
    }
    
    /**
     * カメラ情報を設定
     * @param camera
     */
    private void setCameraParameters(Camera camera) {
        Camera.Parameters parameters = camera.getParameters();
        
        parameters.setPreviewSize(previewPoint.x, previewPoint.y);
        camera.setParameters(parameters);
    }
    
    /**
     * カメラのプレビューサイズ・画面サイズを設定
     * @param camera
     */
    private void initFromCameraParameters(Camera camera) {
        Camera.Parameters parameters = camera.getParameters();
        WindowManager manager = (WindowManager)getApplication().getSystemService(Context.WINDOW_SERVICE);
        Display display = manager.getDefaultDisplay();
        int width = display.getWidth();
        int height = display.getHeight();
        
        if (width < height) {
            int tmp = width;
            width = height;
            height = tmp;
        }
        
        screenPoint = new Point(width, height);
        Log.d(TAG, "screenPoint = " + screenPoint);
        previewPoint = findPreviewPoint(parameters, screenPoint, false);
        Log.d(TAG, "previewPoint = " + previewPoint);
    }
    
    /**
     * 最適なプレビューサイズを設定
     * @param parameters
     * @param screenPoint
     * @param portrait
     * @return
     */
    private Point findPreviewPoint(Camera.Parameters parameters, Point screenPoint, boolean portrait) {
        Point previewPoint = null;
        int diff = Integer.MAX_VALUE;
        
        for (Camera.Size supportPreviewSize : parameters.getSupportedPreviewSizes()) {
            int pixels = supportPreviewSize.width * supportPreviewSize.height;
            if (pixels < MIN_PREVIEW_PIXCELS || pixels > MAX_PREVIEW_PIXCELS) {
                continue;
            }
            
            int supportedWidth = portrait ? supportPreviewSize.height : supportPreviewSize.width;
            int supportedHeight = portrait ? supportPreviewSize.width : supportPreviewSize.height;
            int newDiff = Math.abs(screenPoint.x * supportedHeight - supportedWidth * screenPoint.y);
            
            if (newDiff == 0) {
                previewPoint = new Point(supportedWidth, supportedHeight);
                break;
            }
            
            if (newDiff < diff) {
                previewPoint = new Point(supportedWidth, supportedHeight);
                diff = newDiff;
            }
        }
        if (previewPoint == null) {
            Camera.Size defaultPreviewSize = parameters.getPreviewSize();
            previewPoint = new Point(defaultPreviewSize.width, defaultPreviewSize.height);
        }
        
        return previewPoint;
    }
}

コメント書いてないからわかりづらいですね。すみません。。。
動作としては起動後にカメラプレビューを表示し、画面タッチをした際にオートフォーカス実装後、QRコードの読み込みを開始する。
といったかんじです。
肝となるのはPlanarYUVLuminanceSource。
引数は順に、byte配列の画像データ、プレビューサイズの横幅、プレビューサイズの高さ、読み取りエリアの開始X座標、読み取りエリアの開始Y座標、読み取りエリアの横幅、読み取りエリアの高さ、反転の有無です。

正常に読み込めた場合にはこんなかんじで表示されます。


iOSのほうも試してみました。
http://teru2-bo2.blogspot.jp/2012/07/iosqrzxing-20.html


1 件のコメント:

  1. こんにちは てるてる坊主さんのブログを拝見さてもらい
    QRコードリーダーを作ってみました掲載されていたように
    やってみたらうまく動作しました。それで今は横向きなので
    縦向き固定のリーダー機を作っているのですが、なかなか
    うまくいきません できるだけここのブログにあるファイルを
    元に作りたいのですがご存知のことがありましたら教えてください
    よろしくお願いします

    返信削除