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 にデータを格納するなどして分類結果を保存するなどすればいいでしょう。

今回は以上です。












2017-04-28

Vue.js で年齢をリアルタイム計算(lodashで遅延処理も)

久しぶりの更新です。
今人気が上がってきている Vue.js のウォッチャの勉強したので、まとめておこうと思います。

デモページ


では、まずやり方を以下の2点で説明します。

1.HTML 部分
2.JavsScript 部分


1.HTML 部分

基本的には <input>タグが2つあるだけです。

 <input v-model="birthdate">  
 <input v-model="age">  

特徴は v-model という Vue の命令が入っているだけです。
そして、この中で指定されている「birthdate」「age」という名前が JavaScript 部分で使う変数名になり、birthdate がウォッチャで監視されることになります。

2.JavaScript 部分

     var vm = new Vue({  
       el: '#demo',  
       data: {  
         birthdate: '',  
         age: 0
       },  
       watch: {  
         birthdate: function(val) {  
           this.age = this.calcAge(val);
         }  
       },  
       methods: {  
         calcAge: function(birthdate) {  
           var ageDifMs = Date.now() - birthdate.getTime();  
           var ageDate = new Date(ageDifMs);  
           return ageDate.getUTCFullYear() - 1970;  
         }  
       }  
     })  


とてもとてもシンプルにしたのでデモページのコードとは違っていますが、内容としては次のようになります。

1.data 内は、これから必要になる変数。ということで、さきほど HTML 部分で指定した「birthdate」「age」の2つを設置しましょう。v-model を使用しているので変更があったらリアルタイムでこの中身が更新されます。

2.そして、watch で変数に変化が起こっていないかを監視します。今回は誕生日が変更になったことを知りたいので「birthdate」の中で年齢を計算&変更します。

ちなみに

デモページでは lodash と呼ばれる便利機能をたくさん提供してくれる JavaScript パッケージを使って遅延処理を行っています。(この方が計算してるぞ!感がでるので(笑))

実際には以下のようにします。

 _.delay(function(birthdate) {  
   // ここが0.35秒後に呼ばれる  
 }, 350, val);  

そして、指定時間を待っている間は「enteringFlag」を切り替えることで、「計算中...」というテキストを(表示/非表示)しています。


終わりに

状況によって jQuery を使うほうがスマートな場合もありますが、やはり Vue はなにかと便利です。(^^)
 ではでは。


2017-03-23

機械学習で顔の向きを取得する【scikit-learn】

今回、以前投稿した「顔の入れ替えを試してみた」で取得した顔データからその顔がどっちを向いているのか(角度)?を取得する必要がでてきたので機械学習の scikit-learn を使って実現してみました。
元々は人力で公式を作ろうとしてみましたが、数学の基礎知識が乏しい私には難かったため、これを機に以前からずっと気になっていた機械学習を導入することにしました。

そして、結果としては満足できる精度になりそうになったのでここにその作業の手順を残しておこうと思います。

追記: joblib は使わず pickle を使うようコードを変更しました。 (2017.03.24)




【おおまかな手順】
  • 角度データのある顔画像を用意
  • 顔データを加工して機械学習&結果を保存
  • 学習結果を使って未知の画像から顔の角度を取得

【実際の手順】 

まず角度データつきの顔画像を探しました。
機械学習で一番骨が折れるのはデータ収集です。
ある程度の数がないと精度を高めることができないためです。

ただ、今回はテストということで数は少ないですが以下のページからダウンロードして試してみました。

http://www-prima.inrialpes.fr/perso/Gourier/Faces/HPDatabase.html


次にこの画像から学習データとラベルを取得して学習させます。
(今回は顔が左右どちらにどのくらいの角度向いているかを学習させます。)


学習データ

データは、以下のような顔の右側と左側の比率を使うことにしました。

(左のこめかみから鼻までの距離 + 左のほほから鼻までの距離) / (右のこめかみから鼻までの距離 + 右のほほから鼻までの距離)

※2本ずつの距離を使ったのは縦方向の回転(pitch)の影響が少なくなるのではないかと思ったからです。


ラベル

ラベル(学習後、テストデータを入れると取得できる答え)はダウンロードしたファイル名に顔の角度が「+30」や「-45」などの形で含まれているのでこれを正規表現で切り出してそのまま利用します。

※つまり、ある顔データを入力すると「この顔の角度は+15(左に15度)です」などという形で答えを教えてくれるという形になります。


【実際のコード】

実際の機械学習コードは以下になります。

※このコードを使うには 「顔の入れ替えを試してみた」 で紹介した dlib の顔検出環境が必要になりますので事前に準備しておく必要があります。
(後で知りましたが、dlib は python のパッケージマネージャーの pip や anaconda で楽に準備できるようですね^^;)

※ path/to/***** となっている部分は自分の環境に合わせてください。テストした環境は python 2.7.12 です。

 # -*- coding: utf-8 -*-  
 import sys  
 import cv2  
 import dlib  
 import glob  
 import re  
 import pickle
 import numpy as np  
 from sklearn import svm
 def get_distance(landmarks, ids):  
   id1 = ids[0]  
   id2 = ids[1]  
   point1 = landmarks[id1]  
   point2 = landmarks[id2]  
   return np.linalg.norm(np.array(point2)-np.array(point1))  
 def get_face_h_ratio(landmarks):  
   length1 = get_distance(landmarks, [0,30])  
   length2 = get_distance(landmarks, [16,30])  
   length3 = get_distance(landmarks, [4,30])  
   length4 = get_distance(landmarks, [12,30])  
   return (length1+length3)/(length2+length4)  
 predictor = dlib.shape_predictor('/path/to/shape_predictor_68_face_landmarks.dat') # Predictorファイル  
 detector = dlib.get_frontal_face_detector()  
 train_angles = []  
 train_labels = []  
 for i in range(1,16):  
   dir_path = '/path/to/head_pose_images/Person%02d/*' % i  # ダウンロードした顔画像フォルダ  
   print dir_path  
   paths = glob.glob(dir_path)  
   for path in paths:  
     m = re.search(r'([\+\-]+([0-9]{1,2}))([\+\-]+([0-9]{1,2}))\.jpg$', path)  
     if m:  
       im = cv2.imread(path, cv2.IMREAD_COLOR)  
       rects = detector(im, 1)  
       if len(rects) == 0:  
         continue  
       landmarks = np.matrix([[p.x, p.y] for p in predictor(im, rects[0]).parts()])  # 顔ポイントを取得  
       if landmarks is not None:  
         label = m.group(1)           # ファイル名から顔の左右角度を切り出す(yaw)  
         ratio = get_face_h_ratio(landmarks)   # 顔の左右距離から比率を取得  
         train_angles.append([ratio])  
         train_labels.append(label)  
 clf = svm.SVC()  
 clf.fit(train_angles, train_labels) # 取得したデータで機械学習する  
 pickle.dump(clf, open('yaw_angles.pkl', 'wb')) # 学習した結果を後でも使えるようにファイル保存  

このコードを実行すると「yaw_angle.pkl」というファイルが作成されます。
これが学習結果になりますので後で顔角度を取得したい場合は、以下のようにこのファイルをロードして使うことになります。

 path = 'checking_image.jpg'  
 im = cv2.imread(path, cv2.IMREAD_COLOR)  
 rects = detector(im, 1)  
 if len(rects) == 0:  
   sys.exit()  
 landmarks = np.matrix([[p.x, p.y] for p in predictor(im, rects[0]).parts()])  # 顔ポイントを取得  
 if landmarks is not None:  
   with open('yaw_angles.pkl', 'rb') as f:
      clf = pickle.load(f)
   ratio = get_face_h_ratio(landmarks)   # 顔の左右距離から比率を取得  
   pre = clf.predict([ratio])  
   print pre  # 配列で答え(ここでは角度)が返ってきます。  

※ 注: ここでは import は省略しています。

以上が今回の機械学習の手順になります。
ただし、途中でも書きましたがこれだけでは学習データが少ないと思いますので他のデータも必要になってくるかと思います。

それにしても機械学習はすごいですね。
今回は左右の顔の向き(yaw)でしたが、同じようにすることで上下(pitch)も学習できるでしょうし、顔だけでなくインターネット上に無限にあるデータをうまく学習させればいろいろと面白いことができるのではないでしょうか。

今後は tensorflow の方にもチャレンジしてみたいと思います。
ではまた(^^)

2017-03-13

テキストだけHTMLから抽出する方法を考えてみた【ソースコード・ダウンロード可】

現在の開発でウェブ上のHTMLページから重要なテキストだけを取得する必要がでてきたのでどのようなアプローチがいいのかを考えてみました。

いろいろなアプローチを試しては失敗をしたのですが、最終的にある程度の精度を出せる方法に行きついたのでここで紹介したいと思います。


【抽出アルゴリズム】

実際にはアルゴリズムというほど複雑な方法ではありませんが、結局は「人間の目で見てテキストが密集している部分をグループ化する」というアプローチが功を奏しました。

流れとしては以下になります。
  1. もしテキスト間のHTMLタグが5つ以下なら(つまり近くにあるなら)そのテキストは全てひとつのテキストとして結合させる。
  2. 結合したテキストをひとつひとつチェックし、テキストの長さが100以上あれば、それはコンテンツとして残す。
文章ではわかりにくいと思いますので、HTML タグの例を見てみましょう。

(HTML例)

<div>
    <div>テキスト1</div>
    <div>テキスト2</div>
    <div>テキスト3</div>
</div>

<img src="***">
<img src="***">
<img src="***">

<div>
     <div>テキスト4</div>
</div>


テキストが近いければ結合する

まず、「テキスト1」と「テキスト2」 の間には HTML が2つ(</div>と<div>)だけです。
つまり HTML 構造的にいうと距離は「2ステップ」の位置にあるため近いテキストということになります。
なので、この2つのテキストは結合します。

では、「テキスト3」と「テキスト4」はどうでしょう?
間にあるのは、
</div></div><img><img><img><div><div>
なので7ステップです。

デフォルトの基準は5ステップ以下なら結合することになっているのでこれは「遠いテキスト」ということで結合はしません。

これを全てのテキストで実行すると、ほぼ「見た目で近いテキストが集まったグループ」が作成できることになります。


テキストが長ければコンテンツとして抽出する

グループ別のテキストが作成できたので次にこのテキストがある程度以上長ければ残し、短ければ重要度が低いと判断し削除します。

このフィルターを通過したテキストがコンテンツ・テキストということになります。


ただし

この方法でもパーフェクトではありません。
また、紹介した方法をする前に <br> タグなどを一時的に退避させるなど HTML の加工が必要になりますのでご注意ください。


ソースコードのダウンロード

ということで、このコンテンツ抽出アルゴリズム(細々とした HTML の加工を含んでいます)を PHP クラス「Shellless」として公開しました。
Github でダウンロードか composer でインストールできるかと思いますのでもし興味がありましたらぜひアクセスしてみてください。

https://github.com/SUKOHI/Shellless


今回は以上です。(^o^)




2017-02-28

アフィン変形の他の座標を取得する【Python】

前回の「 Python で画像を引っぱるように歪める」に引き続き python を使ったアフィン変形(回転、縮小/拡大、並行移動を一括で行う変形)のお話です。

アフィン変形は3点を使って回転などをするのですが、数学が苦手な私には「じゃあ、その他の座標はどうやって計算するの?」ということがわかりませんでした。
そこで、今回も備忘録的にこの記事を書きたいと思います。



まずはアフィン変形の使い方(Python)

使い方は簡単で、移動前と移動先の3つの点(つまり三角形)を行列にして変形させるだけ。
コードはこんな感じです。

 import cv2
 import numpy as np
 im = cv2.imread('squares.jpg')  
 points1 = [[50,50],[150,50],[50,150]]  
 points2 = [[75,75],[150,50],[50,150]]  
 M = cv2.getAffineTransform(np.float32(points1), np.float32(points2))  
 im2 = cv2.warpAffine(im, M, (200,200))  
 cv2.imwrite('transformed_squares.jpg', im2)  

(流れ)
1.画像を読み込む
2.移動前の点を決める
3.移動後の点を決める
4.上の2つを使って行列を取得
5.200x200サイズでアフィン変形する
6.画像を保存

※つまり、他の2点は同じなので、(50,50) の点を (75,75)に移動させるだけになります。


実際に画像は以下になります。

(変形前)


(変換後)



移動した後、その他の点は?

さぁ、ではここからが本題です。
先ほど見ていただいた画像の右下に「?」マークと少し大きな赤丸があったかと思います。
この点は変形前は (150,150) の位置にあります。
じゃあ、変形後の座標をどうやって計算したらいいの?を解決する方法です。

実際のコードはこんな形です。

 import numpy as np 
 point = np.float32((150,150))  
 x = point[0]
 y = point[1]
 new_x = M[0][0]*x + M[0][1]*y + np.float32(M[0][2])  
 new_y = M[1][0]*x + M[1][1]*y + np.float32(M[1][2])  
 print (int(new_x), int(new_y))  

変形前の点を公式に当てはめるだけでOKです。

※ M は上のコードで作った行列になります。


終わりに

行列という概念は高校の数学ででくる(らしい)のですが、正直言って今回初見ぐらいの気分でした。
ただ、画像処理にはとても便利なのでやはりきちんと数学(幾何学?)も勉強していこうと思います。
学生の頃は何に役に立つんだ?と思ってましたが、これはとても役に立つし、正直やってて面白いですね。

新しい発見でした(^o^)



2017-02-24

Python で画像を引っぱるように歪める

最近の開発で python の画像処理を勉強しているのですが、なかなか「引っぱるように画像を歪める」ということができませんでした。
いわゆる gimp でいうところの「対話的歪め」とか iwarp というやつです。
そして、それを今回なんとか実現することができましたので、備忘録として残しておこうと思います。

(追記) pip でインストールできるようパッケージ化しました

 PiecewiseDistortion


(つまり画像で言うと↓↓↓)


ということです。

※ちなみにこの処理には scikit-image を利用するので以下のようにインストールをしておいてください。

pip install scikit-image
 
 

まずは手順

手順としては

  1. 歪めたい範囲を指定
  2. どこからどこまで移動させたいかを指定
とするとやりたいことが実現できました。


実際のコード

では実際のコードを紹介します。

import skimage.transform  
from PIL import Image  
import numpy as np  

def distort(file_path, roi_points, from_points, to_points):  
    im = Image.open(file_path).convert('RGBA')  
    from_points = np.concatenate((roi_points, from_points))  
    to_points = np.concatenate((roi_points, to_points))  
    affin = skimage.transform.PiecewiseAffineTransform()  
    affin.estimate(to_points, from_points)  
    im_array = skimage.transform.warp(im, affin)  
    im_array = np.array(im_array * 255., dtype=np.uint8)  
    if im_array.shape[2] == 1:  
        im_array = im_array.reshape((im_array.shape[0],im_array.shape[1]))  
    warped_im = Image.fromarray(im_array, 'RGBA')  
    im.paste(warped_im, (0, 0), warped_im)  
    return im  

file_path = 'lena.jpg'  
roi_points = [(100,100),(400,100),(400,400),(100,400)]  
from_points = [(300,300),(250,250)]  
to_points = [(350,350),(200,200)]  
im = distort(file_path, roi_points, from_points, to_points)  
im.save('lena_distorted.jpg')  


(上のコードを実行した結果↓↓↓)


※画像のサイズは 512x512でした。元画像の lenaさんには申し訳ないですがうまくゆがんでいるのがわかるかと思います。


この作業を通じて思ったこと

どこかの記事で「プログラミング別収入ランキング」というのを読んだことがあるのですが、たしか python が一位でした。
その時はそうなんだ、ぐらいに思っていたのですがその理由が少し分かった気がします。
つまり、

python の収入が高いというよりは、python は数学、科学との連携が強みのため、そういった理数系の知識が豊富な開発は収入が高い

ということになるのだと思いました。
やっぱり理数系の知識って必要ですね。
今回久しぶりに sin cos などを使って懐かしい反面、「え!こんな便利に使えるものなんだ!」と思いました。
学校の勉強がいかに面白くなく感じていたか、、、と思う今日このごろです(笑)

ではでは。