Atom's tech blog

OpenCVでQRCodeスキャンをやってみた

概要

OpenCVのQRCodeスキャンメソッドを使ってwebBowserができるかトライしてみた。今回初めてトライしたのが、QRCode、webBrowser、画像編集のalphaBlend。

QRCode構成説明

QRCodeの詳しいことは知らないため、いい機会なのでサイトに簡単な説明があったので見てみた。誤り訂正もついている事に驚き。よく考えられているなーー。と勉強になった。

名称 内容
最大データ量 数字:7,089字、英数字:4,296字、漢字:1,817字
ファインダパターン(切り出しシンボル) QRコードの3コーナーに配置される3個(マイクロQRは1個)の位置検出用パターン
アライメントパターン 歪みによって生じる各セル(ドット)の位置ずれを補正
クワイエットゾーン 2次元コードシンボルのまわりにある空白の部分。QRコードモデル1、モデル2で4セル分、マイクロQRコードで2セル分の空白が必要
タイミングパターン 白セルと黒セルが交互に配置され、シンボル内のモジュール座標を決定するのに使用
フォーマット情報 QRコードシンボルに、使用されている誤り訂正率とマスクパターンに関する情報
誤り訂正符号(リードソロモン符号) QRコードの一部分が損傷した場合でもデータを損失することなく、復元することができるようにリードソロモン法を用いて生成された符号のこと。復元率は、シンボルの損傷の度合いに応じた4段階のレベル

f:id:iAtom:20201008170743j:plain

QRCode検出の簡単なソースイメージ

QRCode検出用のインスタンスを生成し、そのインスタンスからdetectAndDecodeメソッドを使用してスキャン。結果は以下が返却されるみたい。意外と簡単に実装できそう。

  • 戻り値1:dataはスキャンした結果データ(文字列)、QRCode未の場合は空白。
  • 戻り値2:pointsはQRCodeの座標情報。
# QRCodeDetectorインスタンス生成
QRDetector = cv2.QRCodeDetector()
# QRCode検出
data, points,_ = QRDetector.detectAndDecode(image)

alphaBlend

alphaBlendとは、半透明化のような2つの画像を合成する。この画像編集もOpenCVの「cv2.addWeighted」で実現できる。よくスマートフォンアプリでQRCodeを映すカメラ画像の外枠が透けている感じイメージをPythonでトライしてみました。

処理概要

  • カメラから画像をキャプチャ
  • 黒背景画像とキャプチャ画像をalphaBlendの画像編集
  • QRCodeスキャン用の四角枠をトリミングし、alphaBlend画像の上にペースト
  • QRCodeスキャン用の四角枠の角近くを白線で点滅表示
  • QRCodeスキャン用の四角枠の画像をQRCode検出メソッドに入力
  • QRCode検出結果が「https://」の文字列がある場合は、検出したURLでwebBrowserでアクセス

環境条件

イメージ

f:id:iAtom:20201008170851p:plain

ソースコード

無駄処理やバグがあるかも知れません。ご了承くださいませ。 ソースコード後半がQRCodeスキャンし、URLアクセス。 ソースコード前半のメソッド類は、QRCodeスキャン用のフォーム作成に関連するもの。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cv2
import webbrowser
import numpy as np
from PIL import Image

# URL保持情報初期化
url = ""

# 画像サイズ
_width = 640
_height = 480
# QRcodeサイズ
_qrCodeWidth = 200
_qrCodeHeight = 200
# 白枠表示カウンタと非表示カウンタ
_line_on_cnt = 3
_line_off_cnt = 7

#カメラデバイスオープン
cap = cv2.VideoCapture(0)

# カメラフレームサイズをに変更する
cap.set(cv2.CAP_PROP_FRAME_WIDTH, _width) 
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, _height)

# QRコードDetectorインスタンス生成
QRDetector = cv2.QRCodeDetector()

###########################
# OPENCV -> PIL変換メソッド #
###########################
def cv2pil(image_cv):
    image_cv = cv2.cvtColor(image_cv, cv2.COLOR_BGR2RGB)
    image_pil = Image.fromarray(image_cv)
    image_pil = image_pil.convert('RGB')
    return image_pil

###########################
# PIL -> OPENCV変換メソッド #
###########################
def pil2cv(image_pil):
    image_cv = np.asarray(image_pil)    
    image_cv = cv2.cvtColor(image_cv, cv2.COLOR_RGB2BGR)
    return image_cv

#########################
# 画像中心画像取得メソッド  #
#########################
def crop_center_img(pil_img, crop_width, crop_height):
    img_width, img_height = pil_img.size
    return pil_img.crop(((img_width - crop_width) // 2,
                         (img_height - crop_height) // 2,
                         (img_width + crop_width) // 2,
                         (img_height + crop_height) // 2))

##########################
# 画像中心座標軸取得メソッド #
##########################
def crop_center_pos(pil_img, crop_width, crop_height):
    img_width, img_height = pil_img.size
    x = img_width/2 - crop_width/2
    y = img_height/2 - crop_height/2
    return(x,y)

#####################################
# QRコード検出用位置の角に線を引くメソッド #
#####################################
def edge_line(image_cv,left_high_x,left_high_y,qr_width,qr_height):
    # 起点からの長さ用
    line_width = 30
    # 四角の外側のマージン
    margin = 10
    ##########################
    # 左上のコーナーから横に線   #
    ##########################
    cv2.line(image_cv,
            pt1=(left_high_x-margin           ,left_high_y-margin),
            pt2=(left_high_x-margin+line_width,left_high_y-margin),
            color=(255, 255, 255),
            thickness=2)
    ##########################
    # 左上のコーナーから下に線   #
    ##########################
    cv2.line(image_cv,
            pt1=(left_high_x-margin ,left_high_y-margin),
            pt2=(left_high_x-margin ,left_high_y-margin+line_width),
            color=(255, 255, 255),
            thickness=2)            

    ##########################
    # 右上のコーナーから横に線   #
    ##########################
    cv2.line(image_cv,
            pt1=(left_high_x+qr_width+margin           ,left_high_y-margin),
            pt2=(left_high_x+qr_width+margin-line_width,left_high_y-margin),
            color=(255, 255, 255),
            thickness=2)

    ##########################
    # 右上のコーナーから下に線   #
    ##########################
    cv2.line(image_cv,
            pt1=(left_high_x+qr_width+margin ,left_high_y-margin),
            pt2=(left_high_x+qr_width+margin ,left_high_y-margin+line_width),
            color=(255, 255, 255),
            thickness=2)

    ##########################
    # 左下のコーナーから横に線   #
    ##########################
    cv2.line(image_cv,
            pt1=(left_high_x-margin             ,left_high_y+qr_height+margin),
            pt2=(left_high_x-margin+line_width  ,left_high_y+qr_height+margin),           
            color=(255, 255, 255),
            thickness=2)

    ##########################        
    # 左下のコーナーから下に線   #
    ##########################
    cv2.line(image_cv,
            pt1=(left_high_x-margin ,left_high_y+qr_height+margin),
            pt2=(left_high_x-margin ,left_high_y+qr_height+margin-line_width),
            color=(255, 255, 255),
            thickness=2)  

    ##########################
    # 右下のコーナーから横に線   #
    ##########################
    cv2.line(image_cv,
            pt1=(left_high_x+qr_width+margin           ,left_high_y+qr_height+margin),
            pt2=(left_high_x+qr_width+margin-line_width,left_high_y+qr_height+margin),
            color=(255, 255, 255),
            thickness=2)

    ##########################
    # 右下のコーナーから下に線   #
    ##########################
    cv2.line(image_cv,
            pt1=(left_high_x+qr_width+margin ,left_high_y+qr_height+margin),
            pt2=(left_high_x+qr_width+margin ,left_high_y+qr_height+margin-line_width),
            color=(255, 255, 255),
            thickness=2)


# 背景色黒作成
black_img_pil = Image.new(mode='RGB', size=(_width, _height))
black_img_cv = pil2cv(black_img_pil)

# 白い枠点滅用カウンタ
line_view = 0
# カメラデバイスオープン状態の時ループ
while(cap.isOpened() == True): 
    # カメラキャプチャ
    ret, frame_cv = cap.read()

    ################################
    # QRコードスキャン用フォーム作成    #
    ################################
    #アルファブレンド(黒背景とカメラ画像)
    alpha_blend_cv = cv2.addWeighted(black_img_cv, 0.85, frame_cv, 0.15, 1)
    # Opencv -> PILへ変換
    frame_pil = cv2pil(frame_cv)
    alpha_blend_pil = cv2pil(alpha_blend_cv)
    # カメラキャプチャした中心画像のみ取得(QRコードのみ取得)
    cut_frame_pil = crop_center_img(frame_pil, _qrCodeWidth, _qrCodeHeight)
    # QRコードだけの画像をPIL -> Opencvへ変換(QRコード検出用)
    cut_frame_cv = pil2cv(cut_frame_pil)
    # 背景黒の中心座標位置を取得(imwshow表示用)
    x,y = crop_center_pos(alpha_blend_pil,_qrCodeWidth,_qrCodeHeight)
    # 背景黒イメージにカメラから切り取りした画像の張り付け(imshow用)
    alpha_blend_pil.paste(cut_frame_pil, (int(x), int(y)))
    # PIL -> Opencvへ変換(imshow表示用)
    QRcode_form = pil2cv(alpha_blend_pil)
    
    ####################
    # 白い枠表示/非表示   #
    ####################
    # 表示カウンタ以上
    if line_view >=_line_on_cnt :
        # 白枠表示
        edge_line(QRcode_form,int(x),int(y),_qrCodeWidth,_qrCodeHeight)
    line_view += 1  
    # 表示OFF用カウンタ以上  
    if line_view >= _line_off_cnt:
        # 表示カウンタクリア
        line_view = 0

    ################################
    # QRコード検出&URLアクセス        #
    ################################
    data, bbox,_ = QRDetector.detectAndDecode(cut_frame_cv)
    # データあり
    if len(data) > 0:
        # QRコードデータ表示
        print("Decoded Data : {}".format(data)) 
   
        # HTTPアクセス用のQRコードの場合
        if data.find('https://') != -1 :
            # 違うURLの場合はブラウザアクセスする
            if url != data :
                # URL保存
                url = data
                cv2.imwrite('QRcoee_image.png',QRcode_form)
                # 読み込んだURLでブラウザ起動
                webbrowser.open_new(url)

    # ウィンドウに表示
    cv2.namedWindow("QRCode",cv2.WINDOW_NORMAL)
    cv2.moveWindow("QRCode",200,100) # Window表示位置指定
    cv2.resizeWindow('QRCode', 640,480)
    cv2.imshow('QRCode', QRcode_form)

    # ESCキーで終了  
    k = cv2.waitKey(10) & 0xff   
    if k == 27:  
        break  

# Do a bit of cleanup  
print("\n Exit Program")
cap.release()
cv2.destroyAllWindows()