LoginSignup
338
347

More than 5 years have passed since last update.

iOS 7でバーコード・QRコードを読み取る方法と生成する方法(おまけもあるよ)

Last updated at Posted at 2013-12-04

iOS Advent Calender 2013 iOS second stage 4日目担当の@hkato193です。書籍「iPhoneアプリ開発エキスパートガイド」や「OS XとiOSのためのOpenCV環境構築ガイド」などのiPhoneアプリ開発関係の本を書いたりしています。アプリだと、プロアスリートが練習に使うスキップバックレコーダーを実現した「PastVid」や、弾むボールでリズムを表現する「Metronome Bounce」などを公開しています。

さて、iOS 7ではバーコードやQRコードなどの1次元/2次元コードをカメラで読み取ることができます。さらにQRコードは読み取りだけでなく、Core Imageを使ったコード作成も行えます。本記事では、読み取りと作成の方法をそれぞれ紹介します。

1. 読み取り編

1次元/2次元コードの読み取りは、AVFoundationフレームワークのキャプチャ出力に関連するクラスAVCaptureMetadataOutputクラスの一機能として標準提供されるようになりました。

iOS_cameraoverview.png

コード認識の処理は、顔認識の方法と基本的に同じです。

  1. キャプチャセッションの準備
  2. キャプチャセッションのアウトプットに繋ぐAVCaptureMetadataOutputクラスを作成
  3. setMetadataObjectsDelegate:queue:メソッドを使って、識別結果を受けるデリゲートオブジェクトと、デリゲートメソッドを呼ぶワークキュー(GCDのシリアルキューがベター)を指定
  4. キャプチャセッションのアウトプットに接続
  5. アウトプットに接続後、識別したいコードの種類をmetadataObjectTypesプロパティに指定
  6. キャプチャセッションを開始

という手順です。顔認識の手順とは5番で指定する値が異なるだけですね。これによって識別処理がキャプチャセッションの開始と同時に始まり、フレーム内にコードがあると分かったタイミングで、指定したワークキュー上でデリゲートメソッドが呼ばれ、AVMetadataMachineReadableCodeObjectオブジェクトの配列として読み取り結果が渡されます。あとは得られた情報を元にWEBサイトにジャンプするなり、メッセージを表示するなり、好きな処理を実行するだけです。

キャプチャセッションの準備

実際のコードを見てみましょう。最初は、キャプチャセッションを作成するときのAVCaptureMetadataOutputクラスの初期化処理です。

QRコードとJANコードの2種類を識別するアウトプットの設定例

// メタデータ識別用のオブジェクトを作成
AVCaptureMetadataOutput *metaOutput = [[AVCaptureMetadataOutput alloc] init];

// 識別結果を伝えるデリゲートオブジェクトと処理を行うシリアルキューを指定
//   ※認識したメタデータが順序正しくデリゲートメソッドに届くよう、
//     ワークキューにはグローバルキューではなくシリアルキューを渡すこと
[metaOutput setMetadataObjectsDelegate:self
    queue:dispatch_queue_create("myQueue.metadata", DISPATCH_QUEUE_SERIAL)];

// アウトプットに追加
[self.session addOutput:metaOutput];

// アウトプットに追加後、識別したいコード(QRコード、JANコード標準タイプ)を指定
//   ※顔認識とコード認識の両方を同時に渡しても、どちらも正常に動作する
metaOutput.metadataObjectTypes =
    @[AVMetadataObjectTypeQRCode, AVMetadataObjectTypeEAN13Code]; ←②

注意点としては、metadataObjectTypesプロパティの指定は「 キャプチャセッションにaddOutput:した後 」で、なおかつ「 キャプチャセッションにキャプチャデバイスがaddInput:されている状態 」で行うようにしてください。冒頭の図だと、中央のキャプチャセッションを挟んだ左右のオブジェクトです(プレビューはなくても大丈夫です)。

これは識別できるメタデータの種類はキャプチャデバイスに依存していて、使用するキャプチャデバイスが分からない状況ではメタデータの種類を指定しようがないためです。たとえばキャプチャデバイスにはカメラだけではなくマイクもあります。マイクだけがaddInput:されている状態ではQRコードの識別が出来るはずもありませんので、この状態でQRコードを指定しても何の意味もありません。

metadataObjectTypesプロパティに設定できるタイプと、コード上の表現は次のとおりです。iOS 7時点では、各種コードの中でも特によく使われる10種類をサポートしています。日本でのバーコードと言えばEAN-13や8で、二次元コードだとQRコードが一般的ですね。

コードの種類 定数NSString*
UPC-E AVMetadataObjectTypeUPCECode
Code 39 AVMetadataObjectTypeCode39Code
Code 39 mod 43 AVMetadataObjectTypeCode39Mod43Code
EAN-13(JANコード標準タイプ) AVMetadataObjectTypeEAN13Code
EAN-8(JANコード短縮タイプ) AVMetadataObjectTypeEAN8Code
Code 93 AVMetadataObjectTypeCode93Code
Code 128 AVMetadataObjectTypeCode128Code
PDF417 AVMetadataObjectTypePDF417Code
QRコード AVMetadataObjectTypeQRCode
Aztec code AVMetadataObjectTypeAztecCode
(参考)人の顔 AVMetadataObjectTypeFace

認識させたいコードは同時に複数種類を登録でき、また画面内に複数個のコードがあった場合も同時(最大4個)に認識します。

ちなみに現在設定可能なメタデータの種類は、AVCaptureMetadataOutputクラスのavailableMetadataObjectTypesプロパティを使って取得できるので、この中に含まれるタイプだけを指定するようにするとプログラマブルでベターです。

iOS 6 SDKではデフォルトでAVMetadataObjectTypeFaceが指定された状態になっていて、何もしなくても顔認識を開始できたのですが、iOS 7 SDKを使うとデフォルトでmetadataObjectTypesプロパティは空の配列が設定されていて、AVMetadataObjectTypeFaceは含まれていません。既存アプリをiOS 7に対応させる場合は、このデフォルト動作の違いにも注意が必要です。

コードの識別結果を取得

キャプチャデータ中のコードが識別できると、準備のときにsetMetadataObjectsDelegate:queue:メソッドで登録したデリゲートオブジェクトのcaptureOutput:didOutputMetadataObjects:fromConnection:メソッドを呼びます。そのときのコードを見てみましょう。

以下のコードは、識別結果のうちQRコードとJANコードに注目して、QRコードの場合はURLとして開き、JANコードの場合はISBNコードかどうかを見極めた上でamazonのページを開く処理です。

識別できたコードを元にWEBページを開く(書籍の場合はAmazonを開く)

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection
{
    // 認識されたメタデータは複数存在することもあるので、1つずつ調べる
    for (AVMetadataObject *data in metadataObjects) {

        // 一次元・二次元コード以外は無視する
        // ※人物顔の識別結果だった場合など
        if (![data isKindOfClass:[AVMetadataMachineReadableCodeObject class]])
            continue;

        // コード内の文字列を取得
        NSString *strValue =
            [(AVMetadataMachineReadableCodeObject *)data stringValue];

        // 何のタイプとして認識されたかを確認
        if ([data.type isEqualToString:AVMetadataObjectTypeQRCode]) {

            // QRコードの場合、URLとしてmobileSafariを開く
            NSURL *url = [NSURL URLWithString:strValue];
            if ([[UIApplication sharedApplication] canOpenURL:url]) {
                [[UIApplication sharedApplication] openURL:url];
            }
        } else if([data.type isEqualToString:AVMetadataObjectTypeEAN13Code]) {

            // JANコード(標準タイプ)の場合、ISBNかどうかを調べて
            // ASINコードに変換し、対応するAmazonのページを開く
            long long value = strValue.longLongValue;
            NSInteger prefix = value / 10000000000;
            if (prefix == 978 || prefix == 979) { // ISBNかどうかをチェック
                long long isbn9 = (value % 10000000000) / 10;
                long long sum = 0, tmp_isbn = isbn9;
                for (int i=10; i>0 && tmp_isbn>0; i--) {
                    long long divisor = pow(10, i-2);
                    sum += (tmp_isbn / divisor) * i;
                    tmp_isbn %= divisor;
                }
                long long checkdigit = 11 - (sum % 11);

                // asinコードに変換
                NSString *asin =
                    [NSString stringWithFormat:@"http://amazon.jp/dp/%lld%@",
                     isbn9,
                     (checkdigit == 10)?
                        @"X":[NSString stringWithFormat:@"%lld", checkdigit%11]];
                [[UIApplication sharedApplication]
                 openURL:[NSURL URLWithString:asin]]; // URLのオープン
            }

        }
    }
}

コードの認識結果を扱うAVMetadataMachineReadableCodeObjectクラスはAVMetadataObjectクラスのサブクラスなので、AVMetadataObjectクラスが持つ属性の他に「cornersプロパティ」と「stringValueプロパティ」の2つを持っています。stringValueプロパティが格納されていた文字列を示し、cornersプロパティの情報は、コードの四隅がどこにあるかを示す座標を、正しい向きでの左上から逆時計回りに格納した情報です。よく似たプロパティとしてboundsプロパティがAVMetadataObjectクラスにありますが、こちらは画面上のコードが内接する矩形を示した情報を表すためのプロパティで、意味合いが異なります。

1次元コードは2次元コードと異なり、キャプチャデータの読み取り範囲が画面中央の一部に限定されています。アプリを作る場合は、画面上に帯を示すUIをもうけるようにしておくとUXの向上に繋がります。

2. コード生成編

続いてQRコードの作成方法です。こちらはCore ImageのCIQRCodeGeneratorフィルターを使って作成します。

実はiOS 6にもQRコード生成フィルターは用意されていてPassbookアプリでも使われていたのですが、Appleのプライベートフィルタとして分類されていたため、公式利用はできませんでした。

「格納したいデータ」と「誤り訂正レベル」の2つを設定するだけで、使用するQRコードのバージョンを自動的に判断し、出力画像には必要分のマージンも含めてくれる機能を持っています。

作成方法

以下のコードは、文字列「Hello, iOS7 world!」を含めたQRコードを作成し、画面上のUIImageViewに表示するまでの処理です。

QRコードを作成し、画面上のUIImageViewに表示

// QRコード作成用のフィルターを作成・パラメータの初期化
CIFilter *ciFilter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
[ciFilter setDefaults];

// 格納する文字列をNSData形式(UTF-8でエンコード)で用意して設定
NSString *qrString = @"Hello, iOS7 world!";
NSData *data = [qrString dataUsingEncoding:NSUTF8StringEncoding];
[ciFilter setValue:data forKey:@"inputMessage"];

// 誤り訂正レベルを「L(低い)」に設定
[ciFilter setValue:@"L" forKey:@"inputCorrectionLevel"];

// Core Imageコンテキストを取得したらCGImage→UIImageと変換して描画
CIContext *ciContext = [CIContext contextWithOptions:nil];
CGImageRef cgimg =
    [ciContext createCGImage:[ciFilter outputImage]
                    fromRect:[[ciFilter outputImage] extent]];
UIImage *image = [UIImage imageWithCGImage:cgimg scale:1.0f
                               orientation:UIImageOrientationUp];
CGImageRelease(cgimg);

// 画面のUIImageViewに表示
self.imageView.image = image;

CIQRCodeGeneratorフィルターは次に示す2つのパラメータを持っています。

  • inputMessage:QRコードに格納するデータ。NSData形式
  • inputCorrectionLevel:誤り訂正レベル。NSString形式

inputMessage」パラメータに文字列「Hello, iOS7 world!」を設定していますが、inputMessageはNSData形式のデータを渡す必要があるため、いったんUTF-8でエンコードしたNSDataオブジェクトに変換してから設定しています。「inputCorrectionLevel」パラメータは、仕様と同じ「L」〜「H」のいずれか1文字をNSStringで設定します。Hに近づくほど冗長性が確保され、表面の一部が汚れて読み取れないようなときの耐性が上がります(その代わり、出来上がるQRコードは大きく、複雑になります)。

たったこれだけで、任意のデータを含むQRコードが作成できます。

生成したQRコードを表示するときの落とし穴

さて、これを画面上のUIImageViewにそのまま表示すると2つの問題がある問題に気付きます。それは、Core Imageで生成されるQRコードの画像が非常に小さいことで、もう1つは小さい画像をUIImageViewに拡大表示しても見にくいか、あるいは拡大表示してもぼけた状態で拡大されてしまうことです。

fig_06-02-01_03a-tooSmall.png ← 画面だと見やすいけれど、実際は等倍表示で小さすぎる…
fig_06-02-01_03b-fuzzy.png ← 拡大してもボケボケ…

前者のQRコードが小さい問題ですが、これは設定変更などでは解決できないので、使用するときに拡大して表示するなどしなければいけません。後者の「小さな画像を大きく表示するときのスケーリング結果が汚い」という問題は、おもに2つの方法で解決できます。1つがCGContextSetInterpolationQuality()関数を使いつつUIImageをリサイズする方法で、もう1つがUIImageViewのレイヤーにあるmagnificationFilterプロパティを使って、スケーリングの補間アルゴリズムを切り替える方法です。

それぞれは次のようなコードで実現します。先のサンプルコードで、UIImageViewに表示する直前部分にどちらかを記述することを想定しています。

境界をボケさせずに描画する方法2つ

// その1:UIImageを最近傍法を適用しつつリサイズする
UIGraphicsBeginImageContext(CGSizeMake(300, 300));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetInterpolationQuality(context, kCGInterpolationNone);←補間方法の指定
[image drawInRect:CGRectMake(0, 0, 300, 300)];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// その2:UIImageViewの描画方法を最近傍法に切り替える
self.imageView.layer.magnificationFilter = kCAFilterNearest;←補間方法の指定
self.imageView.contentMode = UIViewContentModeScaleAspectFit;

このどちらかの対策を施すことによって、次のように鮮明な画像を任意サイズで表示できます。

fig_06-02-01_04-idealOutput.png ←くっきり!

便利ですね。

3. おわりに

AVFoundationフレームワークもCore Imageフレームワークも、バージョンを重ねるたびに実現出来ることがどんどん増えている、とても強力なフレームワークです。ぜひいろいろな用途に活用してみてください。

明日以降のiOS Second Stage Advent Calendar 2013もお楽しみに!

One more thing

iOS 7 SDKの新機能を踏み込んで説明した書籍「上を目指すプログラマーのためのiPhoneアプリ開発テクニック iOS 7編」が発売されることになりました!

上を目指すプログラマーのためのiPhoneアプリ開発テクニック iOS 7編 (カバーデザインは変更される可能性があります)

「iOS 7 SDKって何ができるの?どう使いこなしたらいいの?」「iOS 7のアプリ作成で、どういう点に気をつけたらいいの?」などに応える、中級〜上級者でも満足できる内容に仕上がっています。
Amazonではすでに予約が始まっていますので、ぜひぜひご覧くださいませ。

明日のiOS Advent Calender 2013 iOS second stageもお楽しみに!

338
347
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
338
347