2017-05-22

Vue + jQuery の弱点に初めて遭遇!

このところ Vue.js を使って開発効率が格段に上がってきているのを感じています。しかも以前の Vue.js 2.0 を使ってみた9つの感想 でも取り上げましたが、jQuery との衝突が全く無かったので機嫌よく Vue での開発を進めていたのですが、今回初めて衝突というか弱点に遭遇したのでまとめと対処法をお届けしたいと思います。



まずは遭遇した状況から。
jQuery(Bootstrap) を使ったページ内で JavaScript を使って input 内の値を変更しようとしました。(※詳しく言うと、後でも言及する datepicker を使ってテキスト内に日付を入力しようとしてました。また、冗長になるので、 this.input = 'xxxxxx'; を使うのは無しの方向です。)

私の頭の中では以下のように通常通り $('#id').val('xxxxx'); を使ってテキストの中身を変更すれば Vue の方でもデータが更新されるものだと思っていました。
しかし、以下のデモのようにテキスト内は変更できるものの、Vue の方では変更は全くないという状況でした。

Vue + jQuery の change イベント実験 - 1



、、、なぜだろう。
jQuery の val() でデータを変更しても change イベントは呼ばれないというのは知っていました(参考ページ)が、今回使っているのは vue の v-model なので問題はないはずなのに・・・?


そこで少し stackoverflow を探ってみると、vue には $forceUpdate() という強制的にデータを更新する方法があるよ、ということなので以下のページのように実際に試してみました。

Vue + jQuery の change イベント実験 - 2


でも、これもうまくいかない、、、、
クリックを続けているとたまに自動入力されるテキストがちらつくことがあるので、おそらく変更はされたが、$forceUpdate() によって瞬間的に元にもどされてしまっているという状況なのだろう(つまり Vue には伝わっていない)と思いました。


またしてもスタート地点に戻ってしまったので、もう一度いろいろとネット上の情報を探ってみると本家 GitHub の Issue で手がかり見つけました。

Triggering vuejs on programatical change of element.


どうやら、jQuery の changeイベントはネイティブの JavaScript のイベントとは別のものなので、もし Vue の変更をやりたいならネイティブイベントを作って dispatch しないといけないということでした。

そこで、早速以下のようにイベント送出をしてみました。

Vue + jQuery の change イベント実験 - 3


おっ、いけました!
この状態だと直接テキスト入力してもボタンをクリックしても Vue のデータが書き換わるのでリアルタイムに上のテキストも変更されるようになっています。

へぇ、Vue にはネイティブ JS のイベントが必要だったんですね。


では、本題の datepicker を使うにはどうすればいいのでしょうか。
今回は jQuery の changeイベントとのコラボでやってみました。

$('.datepicker').datepicker().on('change', function() {

    var event = document.createEvent('HTMLEvents');
    event.initEvent('input', true, true);
    $(this).get(0).dispatchEvent(event);

});

実際のテストは以下です。

Vue + jQuery の change イベント実験 - 4


やってみたら分かっていただけると思いますが、テキスト入力(数字しか入力できません)でもカレンダー選択のどちらでも Vue へデータが伝わっていると思います。

※ちなみに送出するイベントが「input」ではなく「change」の場合だと、これもうまくいきませんでした。

ということは、この方法を使えば「dispach-native-event」などのクラス名に changeイベントを作っておき、その中でネイティブ・イベントを送出することができるので、サイト全体での対処も比較的楽にできるかと思います。


んー、それにしてもこんな形で Vue + jQuery の問題点に遭遇するとは思いませんでした。
でもその他の部分では問題もなく、開発速度も上がり、保守もしやすいと思うので Vue の利用は続けていこうと思います。


【あとがき】

フロントエンド界の活動は近年ホントに活発でそれ自体はとてもいいことなのですが、正直なところをいうとたくさんありすぎてもう少し集約してほしい気はしています。
また、複雑さが年を追うごとにひどくなってきていて、「あれ、なんのためのフレームワークだったけ?開発効率、ほんとにこれで上がってる??」なんてことになってきているなぁ、と pythonプログラミングをしているとよく感じます。

この間どこかのブログで「3年後も react 使ってると思う? vue 使ってると思う? でも確実に jQuery は使ってるよね?」という趣旨の記事を読みました。

これには Vue が大好きな僕でも「確かにそれはあるかもー」と思いました。
そんなこんなもあるので、変化の速い(早すぎる)これからのプログラミングは学習コストの低さも重要なファクターの一つになってくるんじゃないかな、と今日出してきた扇風機にあたりながら考えてました。
(もちろん何を専門にするかで変わってくるでしょうけどね。)


それはともかく、今年の夏は涼しかったらいいなー(笑)




2017-05-09

Python でバーコードをスキャン(ソースコード・ダウンロード可)

今回は、たくさんの商品画像の中にあるバーコードの内容をプログラムでスキャンして取得&その写真の分類ができるようにしてみたいと思います。
利用するのは次の3つです。

1.Python(2.7.12)
2.OpenCV2
3.Zbar

※実際のコードは GitHub からダウンロードできます。


【準備】

まずバーコードをスキャンするには zbar というパッケージが必要になりますのでインストールをしましょう。Ubuntu だと以下のコマンドで zbar をインストールできます。
 sudo apt-get install libzbar-dev  
そして、python から zbar が使えるようにするため pip でインストールです。
 pip install zbar  
はい。
これでインストールは完了です。(OpenCVはメジャーなのでインストールは省きます。)

【基本編】

では、zbar を使って画像の中にあるバーコードをスキャンする簡単なコードを作っていきましょう。
 import cv2  
 import zbar  

 scanner = zbar.ImageScanner()  
 scanner.parse_config('enable')  
必要なパッケージをインポートして、zbar のスキャナーを作成します。

 im = cv2.imread('images/barcode1.jpg')  
 gray_im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)  
OpenCV でスキャンしたい画像を読み込んでグレースケールに変換します。

 rows,cols = im.shape[:2]  
 image = zbar.Image(cols, rows, 'Y800', gray_im.tostring())  
 scanner.scan(image)  
ここで zbar の image を作成します。
1行目で画像のサイズとさっき作った gray_im をセットして実際にスキャンを開始します。

 for symbol in image:  
   print 'Type: %s, Data: %s' % (symbol.type, symbol.data)  
スキャンした結果を表示します。
type には 「qrcode」や「isbn10」などのデータタイプ、そして data には読み取ることができた値が入っています。

以上が簡単な例でした。
ただ、この例ではバーコードが斜めになっている場合は、検出できないことがあります。
そこで、次の応用編では斜めになったバーコードにも対応できるようにしてみましょう。

【応用編】

 import cv2  
 import zbar  

 scanner = zbar.ImageScanner()
 scanner.parse_config('enable')

 im = cv2.imread('images/barcode2.jpg')  
 gray_im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)  
 rows,cols = im.shape[:2]  
ここまでは基本編のおさらいです。
(ちなみに barcode2.jpg ではわざとバーコードがななめになっています。)

では、ここから斜めになったバーコードに対応するため輪郭を取得していきましょう。
 ret,threshold_im = cv2.threshold(gray_im, 150, 255, cv2.THRESH_BINARY)  
 im,contours,hierarchy = cv2.findContours(threshold_im, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  
まず、しきい値を使って画像を白と黒だけに変換します。
そして、findContours() で輪郭を取得します。

 for contour in contours:  
   rect = cv2.minAreaRect(contour)  
   center_pt = (int(rect[0][0]), int(rect[0][1]))
   w = int(rect[1][0])  
   h = int(rect[1][1])  
   angle = int(rect[2])
次に、取得した contours を for ループで回して一つ一つの長方形を取得しましょう。
minAreaRect がその部分になります。

rect は 中心座標、横幅、高さ、傾きのデータを持っているのでこれらをわかりやすいようにひとつひとつ変数(center_pt, w, h, angle)に格納していきます。

 M = cv2.getRotationMatrix2D(center_pt, angle, 1)
 rotated_im = cv2.warpAffine(im.copy(), M, (cols,rows))  
長方形の傾き(角度)が分かったのでこれを使って画像を回転させましょう。
一行目は行列を作って2行目で回転させた画像を取得しています。

 zbar_image = zbar.Image(cols, rows, 'Y800', rotated_im.tostring())  
 scanner.scan(zbar_image)  
そして、さっきと同じく zbar の image をつくります。

 for symbol in zbar_image:  
     symbol_type = symbol.type  
     symbol_data = symbol.data  
       if(symbol_type not in scanned_data.keys()):  
           scanned_data[symbol_type] = []  
           if symbol_data not in scanned_data[symbol_type]:  
               scanned_data[symbol_type].append(symbol_data)  
for ループで回して、もし scanned_data に type 別の値が入っていなければデータを格納。(←つまり、重複防止ですね)

これで、 scanned_data の中にスキャンされたバーコードの情報が入っていることになります。
あとは MySQL や JSON にデータを格納するなどして分類結果を保存するなどすればいいでしょう。

今回は以上です。