Livebookで QRコードと実験

観光客向けに日本のプロ野球を中心としたコンシェルジュチケットサービスを運営しています。どんどん、チームは QRコードのチケットをますます採用しており、多言語対応のカスタムチケットを作成する可能性が開かれています。

例えば、次のチケットを考えてみましょう。

![2023年5月4日阪神タイガースブリーズシートチケットの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/7080473c-87cb-c506-7669-fd2be88f4675.png)

上は、英語翻訳を追加する前の典型的な阪神タイガースの電子チケットの外観です。

実現したいのは、顧客向けにチケット情報をデータベースに入力し、顧客向けには英語でモバイル端末に表示して、球場の案内係向けには日本語で表示できるようにしたいということです。現在、バックアップとして、次のようなよりわかりやすい方法でチケットを印刷しています。

![翻訳された2023年5月4日の阪神タイガースブリーズシートチケットの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/5d4de237-56ca-c5b7-507b-ab1658dd3fe7.png)

手動で作成しているため、英語を追加したのオリジナルと非常によく似ています。それぞれチームは独自のデザインをしているので、同一形式なスタイルQRコードチケットを印刷できればいいなと思います。

そのためのに、QRコードの内容値を読み取れて、再現出力できる可能な方法を調べてみましょう。[Livebook](https://livebook.dev/)を利用して、それがどのように機能するかを理解してみましょうと試してみましょう。

## QRコード遊び場

上記の QRコードをスキャンする場合は、その内容が `202302088808H19425` であることがわかります。よっし。しかし、画像を保存せずに、QRコードの読み取りと作成を自動化できるようにしたいと考えています。そのために、新しい Livebook を開いて、`:evision` と `:kino` をインストールして、実験してみましょう。

```elixir
Mix.install([
   {:evision, "~> 0.1.33"},
   {:kino, "0.11.0"}
])
```

次に、上記の最初のタイガース チケットの画像をLivebook の `Files` セクションの `+ Add File` 領域にドラッグ アンド ドロップします。

![ファイルを追加するの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/2b769bf8-ced1-479c-4c3e-576986f4c08d.png)

これにより、現在の Livebookセッションに添付ファイルとして追加されます。では、添付されたファイルを読み込んでみましょう。

```elixir
alias Evision, as: Cv

Kino.FS.file_path("2023-05-04-han-breeze-35-97.png")
|> Cv.imread()
```

`Evision` は `Cv` ("Computer Vision" のこと?) というエイリアスとしてよく使われるそうので、使用します。

添付された PNGファイルのパスを取得し、`Evision` の `imread/1` 関数にパイプするだけです。次のような結果が得られます。

![電子チケットの画像を読み取るの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/499fc08b-6c37-aecc-cecb-8526f13a025b.png)

次に、`Cv.QRCodeDetector` にパイプして、QRコードの内容を取得しましょう。

```elixir
|> then(&Cv.QRCodeDetector.detectAndDecode(
   Cv.QRCodeDetector.qrCodeDetector(), &1))
```

![画像内のQRコードを検出してデコードするの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/dfb5d27c-48ab-4cc2-a734-9601d29a1585.png)

QRコードのデコードされた文字列がこの結果に表示されています。後で使用するために抽出しましょう。

```elixir
alias Evision, as: Cv

{decoded_string, points, straight_qrcode} =
  Kino.FS.file_path("2023-05-04-han-breeze-35-97.png")
  |> Cv.imread()
  |> then(&Cv.QRCodeDetector.detectAndDecode(
    Cv.QRCodeDetector.qrCodeDetector(), &1))

decoded_string
```

![画像を読み取り、QRコードを検出してデコードし、デコードされた文字列を出力しますの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/836d77e5-772a-f931-5756-05aa479a6630.png)

## QRコードを出力する

これで、デコードされた QRコードの内容が得られ、ゲート、通路、列、座席番号とともにデータベースに保存できるようになりました。QRコード内容にその情報が含まれていると思いますが、それを解読することに興味はありません。QRコードを任意のサイズで再現して様々な方法で出力したいだけです。

そのためには、`Evision` モジュールを使用すると非常に簡単になります。

```elixir
Cv.QRCodeEncoder.encode(Cv.QRCodeEncoder.create(), decoded_string)
```
![デコードされた文字列からQRコードを作成するの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/8cff770f-e471-921f-8895-0ada9e02d215.png)

これで QRコードを出力しましたがが、かなり見にくいです。小さすぎて、上の圧縮画像ではスキャンするはできないと思います。もう少し大きくで出力しましょう。

```elixir
|> Cv.resize({300, 300})
```

![生成された QRコードを拡大したの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/20374b88-57d7-9137-d59b-3ecbeccc4fae.png)

うまく大きくなりまして、スキャンするとは可能です。でも、なんだかぼやけてますね。このために特別に設計された画像補間アルゴリズムを使用して、もっとはっきり出力しましょう。

```elixir
|> Cv.resize({300, 300}, interpolation: CV.Constant.cv_INTER_AREA())
```

![生成された拡大QRコードを鮮明化の図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/9651e95a-0f2d-9710-01cc-090e5db68c4c.png)

はるかに優れており、シャープです。実際、上記の翻訳されたチケットに使用した QRコードはこの作成されたより少しぼやけていることがわかります。このワークフローにより、任意のサイズで鮮明に出力できるようになります。

## 複雑な QRコード

タイガーズの QRコードはどれもシンプルでベーシックな QRコードです。他のほとんどすべての球団は、り複雑で大きな QRコードを使用しています。以下がスワローズの代表的なチケットです。

![翻訳しました 2023年10月4日のヤクルトスワローズSSチケットの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/1febf588-3df4-effa-2d1c-8c8521eb37fe.png)

以上の画像を Livebookに添付して、試してみましょう。

```elixir
{decoded_string, points, straight_qrcode} =
  Kino.FS.file_path("2023-10-04-yak-ss1-22-18.png")
  |> Cv.imread()
  |> then(&Cv.QRCodeDetector.detectAndDecode(
    Cv.QRCodeDetector.qrCodeDetector(), &1))

decoded_string
```

![スワローズチケットのデコードされた文字列を取得するの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/910ff4b3-6c85-f6d0-5bc5-9b1e5961eb60.png)

よっし! ここまでは順調ですね。それでは、QRコードを再生成してみましょう。

```elixir
Cv.QRCodeEncoder.encode(Cv.QRCodeEncoder.create(), decoded_string)
|> Cv.resize({300, 300}, 補間: Cv.Constant.cv_INTER_AREA())
```

![再生された QRコードの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/c20f0865-08df-6947-0429-2caf8b74f2e7.png)

うーん。QRコードを再作成しました。しかし、オリジナルの QRコードとは見えません。新しい QRコードをスキャンすると、同じデコードされた文字列になります。

```elixir
"d34a78f948c92e681f66b34b7c34806eeaee98709e3ae76a69d620103d24fb37"
```
ちょっと Base 16でエンコードされた文字列のようです。UTF-8 またはシフト JIS でしょうか? いいえ、`d34a` は UTF-8で「퍊」の韓国語の文字であって、シフトJISの最初のバイトが `0xd3` であるものはありません。

じゃ、Base 16でエンコードされた文字列をバイトの配列に変換し、QRコードを生成して見てみましょう。

```elixir
{decoded_string, points, straight_qrcode} =
  Kino.FS.file_path("2023-10-04-yak-ss1-22-18.png")
  |> Cv.imread()
  |> then(&Cv.QRCodeDetector.detectAndDecode(Cv.QRCodeDetector.qrCodeDetector(), &1))

decoded_string
|> Base.decode16!(case: :lower)
|> IO.inspect()
|> then(&Cv.QRCodeEncoder.encode(Cv.QRCodeEncoder.create(), &1))
|> Cv.resize({300, 300}, interpolation: Cv.Constant.cv_INTER_AREA())
```

![デコード値の HEX デコード後に再生成されたスワローズ チケット QRコードの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/e5029a97-ade4-0fc8-98f7-e04f4cf0e3b4.png)

いやー。最初の QRコードから再生された QRコードにデータがすこじまばされたそうでしたが、もっとデータが減られたそうに見えます。

より大きな QR コードを機能させるには、さらに研究する必要があるようです。ここで何が起こっているのか知っている人がいたら、ぜひご意見をいただきたいです。