Streamlit x Google Books API で簡単な検索ツールを作成する

Google Books API の使い方を勉強するついでにGUIを実装したのでメモ

実装

import requests
import streamlit as st

URI = "https://www.googleapis.com/books/v1/volumes"
NO_IMAGE_URI = "https://1.bp.blogspot.com/-7DsADfq2BX4/Xlyf7aSybcI/AAAAAAABXq8/ut72jfLtCuo8ZvRGp1kqCYEbeQ0dOR8pgCNcBGAsYHQ/s1600/no_image_tate.jpg"  # noqa


def main():
    st.title("Google Books API Search")
    query = st.text_input("search query")

    if query:

        resp = requests.get(URI, params={"q": query})

        if resp.status_code == 200:
            for item in resp.json()["items"]:
                info = item["volumeInfo"]

                title = info.get("title", "")
                subtitle = info.get("subtitle", "")
                authors = info.get("authors", "")
                desc = info.get("description", "")
                thum = info.get("imageLinks", {}).get("thumbnail", "")

                col1, col2 = st.beta_columns([1, 4])
                with col1:
                    if thum:
                        st.image(thum)
                    else:
                        st.image(NO_IMAGE_URI)
                with col2:
                    st.markdown(
                        f"""
                        タイトル: {title} {subtitle}\n
                        筆者: {', '.join(authors)}\n
                        概要: {desc}
                    """
                    )


if __name__ == "__main__":
    main()

GUI

実際の画面, 画像が無いものはいらすとや様の no image を使用させて頂いた.

例では "オライリー"と検索した結果が表示されている.

f:id:sh1m088io:20210715010102p:plain

参考/参照

リアルタイムOCR [メモ]

リアルタイムでOCRを行うデモをstreamlitを使用して作成するメモ

環境

  • python (3.8)
    • pyocr (0.8)
    • Pillow (8.2.0)
    • opencv-python (4.5.2)
    • streamlit (0.83.0)
  • tesseract (4.1.1)
  • ウェブカメラ (C270n)

コード

import cv2
from PIL import Image
import pyocr
import pyocr.builders
import streamlit as st

if __name__ == "__main__":

    st.title("my app")
    state = st.checkbox("Check me out")
    tools = pyocr.get_available_tools()
    ocr = tools[0]

    live_view = st.empty()
    draw_view = st.empty()
    text_area = st.empty()

    cap = cv2.VideoCapture(0)
    while cap.isOpened and state:
        ret, frame = cap.read()
        time.sleep(0.01)

        im = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))

        live_view.image(im)

        lines = ocr.image_to_string(
            im, lang="jpn_vert_best", builder=pyocr.builders.LineBoxBuilder()
        )

        texts = []
        for line in lines:
            # line の描画
            (l, t), (r, b) = line.position
            cv2.rectangle(frame, (l, t), (r, b), (0, 0, 255), 10)
            cv2.line(frame, (l, (t + b) // 2), (r, (t + b) // 2), (0, 0, 255), 10)

            # word box の描画
            texts.append(line.content)
            for box in line.word_boxes:
                ul, lr = box.position
                cv2.rectangle(frame, ul, lr, (0, 255, 0), 10)

        text_area.text("\n".join(texts))
        drawd = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))

        draw_view.image(drawd)

streamlit run app/ocr.py

$ streamlit run app/ocr.py

You can now view your Streamlit app in your browser.

Local URL: http://localhost:8501

ブラウザにアクセス

f:id:sh1m088io:20210630221051p:plain

前処理を何もしないとやはり検出が難しい.

Tesseract + PyOCR を使う

Tesseract + PyOCR を使うためのメモ

環境

インストール

teseract

tesseract のインストール

$ sudo apt install tesseract-ocr libtesseract-dev

インストールの確認

$ tesseract -v 
tesseract 4.1.1
 leptonica-1.79.0
  libgif 5.1.4 : libjpeg 8d (libjpeg-turbo 2.0.3) : libpng 1.6.37 : libtiff 4.1.0 : zlib 1.2.11 : libwebp 0.6.1 : libopenjp2 2.3.1
 Found AVX2
 Found AVX
 Found FMA
 Found SSE
 Found libarchive 3.4.0 zlib/1.2.11 liblzma/5.2.4 bz2lib/1.0.8 liblz4/1.9.2 libzstd/1.4.4

使用できる言語の確認

$ tesseract --list-langs
eng
osd

初期状態だと日本語は利用できない. 日本語の学習済みモデルを用意する必要があるが, 方法は下記の2通りある.

ubuntuリポジトリからインストール

日本語学習済みモデルをインストールするならこれ

$ sudo apt install tesseract-ocr-jpn tesseract-ocr-jpn-vert

脳死で全言語をインストールするならこれ

$ sudo apt install tesseract-ocr-all

インストールされた学習済みモデルファイルは/usr/share/tesseract-ocr/4.00/tessdata/にある.

$ ls /usr/share/tesseract-ocr/4.00/tessdata/
configs/ jpn.traineddata osd.traineddata  tessconfigs/ eng.traineddata jpn_vert.traineddata pdf.ttf

手動でダウンロード

tesseractの学習済みモデルは1つの言語に対して以下の3種類がある.

  • 高精度版 (tessdata_best)
  • 通常版 (tessdata)
  • 高速版 (tesssdata_fast)

高精度版と高速版は公式ページからダウンロードしてくるしか無い(と思う)

ここからダウンロードしてきた学習済みモデルファイルを適当な場所に保存する.

/usr/share以下に手動でファイルを置くことに抵抗がなければ/usr/share/tesseract-ocr/4.00/tessdata/にダウンロードしてきたファイルを置く.

抵抗がある場合は環境変数TESSDATA_PREFIXに学習済みモデルファイルがあるディレクトリを指定すれば良い.

今回は3種類すべて試すため, 高速版はjpn_fast.traineddata, 高精度版はjpn_best.traineddataと名前を変更して使用する.

PyOCR

pip でインストールするだけ

$ pip install pyocr

インストール確認

$ pip list | grep pyocr
pyocr                   0.8

使い方

初期化と確認

初期化と使用可能なツールと言語の確認

import time

from PIL import Image, ImageOps
import numpy as np
import matplotlib.pyplot as plt
import cv2
import pyocr

tools = pyocr.get_available_tools()

for t in tools:
    print(t.__name__)

print('langs:')
for lang in ocr.get_available_languages():
    print(lang)

出力

Tools:
  pyocr.tesseract
  pyocr.libtesseract
langs:
  eng
  jpn
  jpn_best
  jpn_fast
  osd

toolsは推奨順で帰ってくるらしいので先頭(pyocr.tesseract)を使用する.

OCR を試す

実際に使ってみる.

今回は入力画像としてこの画像を使用する. f:id:sh1m088io:20210630212727j:plain 一応簡単にだが処理時間も計測した.

※ 画像を反転している処理が含まれているが, tesseractは画像が2値化されるとき, 文字が黒くなる方が良いらしいのでこの処理を入れている.

import time

from PIL import Image, ImageOps
import numpy as np
import cv2
import pyocr
import pyocr.builders

origin = Image.open('../testdata/title2.jpg')
inv = ImageOps.invert(origin)

for lang in ['jpn', 'jpn_best', 'jpn_fast']:
    start = time.time()
    lines = ocr.image_to_string(
        inv,
        lang=lang,
        builder=pyocr.builders.LineBoxBuilder())
    elapsed = time.time() - start

    img = np.array(origin)

    for line in lines:
        print(line.content)

        # line の描画
        (l, t), (r, b) = line.position
        cv2.rectangle(img, (l, t), (r, b), (0, 0, 255), 10)
        cv2.line(img, (l, (t + b) //2), (r, (t + b) //2), (0, 0, 255), 10)

        # word box の描画
        for box in line.word_boxes:
            ul, lr = box.position
            cv2.rectangle(img, ul, lr, (0, 255, 0), 10)
        ul, lr = line.position

    print('lang:', lang)
    print('elapsed time:', elapsed)
    Image.fromarray(img).save(f'{lang}.jpg')

通常版

コ ン ピ ビ ュ ー
プ ビ ヒ ュ ー タ シ ン ス テ ム の
理 論 と 実 装
lang: jpn
elapsed time: 0.7257387638092041

f:id:sh1m088io:20210630212453j:plain

高精度版

コン ピュ ー
ン ヒ ュー タ シ ス テム の
理論 と 実装
lang: jpn_best
elapsed time: 0.716602087020874

f:id:sh1m088io:20210630212509j:plain

高速版

コン ピュ ー
ン ビ ュー タ シ ス テム の
埋 請 と 実装
lang: jpn_fast
elapsed time: 0.4119837284088135

f:id:sh1m088io:20210630212518j:plain

  • 青枠は検出された文字列の検出枠と中心線
  • 緑枠は検出された文字の検出枠

所感

  • 文字が水平である
  • 2値化しやすい
  • 文字の大きさを揃える

この辺に気を使うとそこそこの精度で検出する. (逆にこの辺をガバると全然検出できない)

参考・引用

Pythonでドラゴン曲線

ドラゴン曲線を描く.

実装

線分の始点から終点へ向かうベクトルを(-)\frac{\pi}{4}回転させて \frac{1}{\sqrt{2}}倍する処理を繰り返すように実装した. 回転させる角度は\frac{\pi}{4}-\frac{\pi}{4}が交互になるようにする.

def dragon(i, s, e):
    res = []
    def R(t):
        t = t *  np.pi / 4 
        return np.array([
            [np.cos(t), -np.sin(t)],
            [np.sin(t),  np.cos(t)]
        ]) / np.sqrt(2)

    def _dragon(gen, t, s, e):
        if gen == 0:
            return

        m = R(t)@(e - s) + s
        _dragon(gen - 1,  1, s, m)
        res.append(m)
        _dragon(gen - 1, -1, m, e)

    res.append(s)
    _dragon(i, 1, s, e)
    res.append(e)

    return np.array(res)

f:id:sh1m088io:20210305001805g:plain
回帰回数による変化

雑記

アニメーションは最初APNGで作成したのだが, はてなブログでは 表示されなかったので泣く泣くgifへ変換した.

参考

FastAPIを使用した画像処理APIの実装

概要

画像を入力とし, 画像を返すAPIFastAPIで実装する. (FastAPIでAPI部分を実装するより, jsでStream APIを使用するほうが大変であった)

実装

準備

$pip install pillow fastapi jinja2 aiofiles python-multipart uvicorn

API側の実装

画像処理の内容は入力された画像をタイル状に並べるだけのかんたんな処理

UploadFileのリストとして受け取り, StreamingResponseとして返す.

データの読み書きはByteIOを使用して行う.

ByteIOを使用してpillowの画像を返す場合は, 返す前にseekで先頭に戻す必要がある.

@app.post('/api/image-processing')
async def create_image_processing(files: List[UploadFile] = File(...)):
    # open image
    bytes_io = BytesIO(files[0].file.read())
    image = Image.open(bytes_io).convert('RGB')

    # image processing
    data = np.array(image)
    h, w, _ = data.shape
    h = int(h // 2) * 2
    w = int(w // 2) * 2
    data = data[:h, :w, :] \
        .reshape(h // 2, 2, w // 2, 2, -1) \
        .transpose(1, 0, 3, 2, 4) \
        .reshape(h, w, -1)
    content = BytesIO()
    Image.fromarray(data).save(content, format='png')
    content.seek(0)

    # response
    return StreamingResponse(content, media_type='image/png')

フロント側の実装

画像を送信して, そのレスポンスのReadableStreamを処理して表示する処理.

MDN Web Docsのサンプルをそのまま使用している.

postBtn.addEventListener("click", () => {
  //
  if (!imageFile) {
    console.error("no image file");
    return;
  }

  const body = new FormData();
  body.append("files", imageFile);

  fetch("/api/image-processing", {
    method: "POST",
    body: body,
  })
    .then((resp) => {
      const reader = resp.body.getReader();

      return new ReadableStream({
        start(controller) {
          return pump();

          function pump() {
            return reader.read().then(({ done, value }) => {
              if (done) {
                controller.close();
                return;
              }
              controller.enqueue(value);
              return pump();
            });
          }
        },
      });
    })
    .then((stream) => new Response(stream))
    .then((resp) => resp.blob())
    .then((blob) => {
      document.getElementById("output-img").src = URL.createObjectURL(blob);
    });
});

出力結果

f:id:sh1m088io:20210301115128p:plain
画像処理API

リポジトリ

github.com

参考資料

FastAPIでバックグラウンド処理

概要

fastapi で重い処理を走らせるときの処理パターン

実装

成功するときの系だけで失敗したときのときの処理を書いていないので注意

流れとしては タスク作成 → タスクの進捗確認 → タスクの結果取得

タスク

時間のかかるタスクとして10秒間sleepするタスクを実装

タスクの結果は単純に result data という文字列を返す

class Task:
    def __init__(self):
        self.id = str(uuid.uuid4())
        self.progress = 0
        self.res = None

    def __call__(self):
        for _ in range(10):
            time.sleep(1)
            self.progress += 10
        self.res = 'result data'


TASKS = {}

タスク作成

重いタスクを作成するAPI

作成したタスクのIDを返す

@app.post('/task', status_code=202)
async def create_task(background_tasks: BackgroundTasks):
    task = Task()
    TASKS[task.id] = task
    background_tasks.add_task(task)
    return {'task_id': task.id}

タスク進捗取得

タスクの進捗度合いを取得するAPI

状態と進捗度合いとタスク結果のURIを返す

@app.get('/task/{task_id}')
async def read_task(task_id: str):
    if task_id not in TASKS:
        return {'message': 'nope'}

    task = TASKS[task_id]

    if task.res is None:
        return {
            'status': 'IN_PROGRESS',
            'progress': task.progress,
            'uri': None
        }
    else:
        return {
            'status': 'SUCCEEDED',
            'progress': 100,
            'uri': f'/task/result/{task.id}'
        }

タスク結果取得

タスクの結果を取得するAPI

@app.get('/task/result/{task_id}')
async def read_result(task_id: str):
    if task_id in TASKS:
        task = TASKS[task_id]
        return {'result': task.res}
    else:
        return {'result': 'nothing'}

リポジトリ

github.com

参考資料

AVR-Rust + ATtiny13A でLチカ

AVR-Rustが公式にマージされた.
せっかくなので使ってみたいが, Arduinoチュートリアルで使用していてつまらない.
なので今回は秋月電子で購入可能な格安AVRマイコン ATtiny13 をAVR-Rust を使用してLチカした.

主な実行環境

  • OS: Ubuntu 20.04.1 LTS
  • cargo: 1.47.0-nightly
  • avrdude: 6.3-20171130
  • AVRマイコン: ATtiny13A
  • シリアル変換モジュール: FT232RL

  • 抵抗とLED: 床に落ちてたヤツ

今回の実装は以下のリポジトリにある github.com

書き込む環境の準備

まずATtiny13aにプログラムを書き込む環境を準備する必要がある.

私はここで大きく躓いた.

ポート指定にシリアルポート/dev/ttyUSB0を指定すれば良いと思ったがうまく行かない. どうやらシリアルナンバを指定する必要があったようだ.

シリアルナンバはdmesgで調べることができる.

$ dmesg
...
[44085.473422] usb 3-2: new full-speed USB device number 10 using xhci_hcd
[44085.626927] usb 3-2: New USB device found, idVendor=0403, idProduct=6001, bcdDevice= 6.00
[44085.626931] usb 3-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[44085.626934] usb 3-2: Product: FT232R USB UART
[44085.626936] usb 3-2: Manufacturer: FTDI
[44085.626937] usb 3-2: SerialNumber: A505SYG5
[44085.652102] usbcore: registered new interface driver usbserial_generic
[44085.652109] usbserial: USB Serial support registered for generic
[44085.655738] usbcore: registered new interface driver ftdi_sio
[44085.655746] usbserial: USB Serial support registered for FTDI USB Serial Device
[44085.655838] ftdi_sio 3-2:1.0: FTDI USB Serial Device converter detected
[44085.655870] usb 3-2: Detected FT232RL
[44085.656193] usb 3-2: FTDI USB Serial Device converter now attached to ttyUSB0

シリアルナンバはA505SYG5であるとわかった.

シリアルポート設定が判明したところで正しく接続できるか確認する.

$ sudo avrdude -c diecimila -p t13 -P usb:A505SYG5 -B 4200

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.12s

avrdude: Device signature = 0x1e9007 (probably t13)

avrdude: safemode: Fuses OK (E:FF, H:FF, L:6A)

avrdude done.  Thank you.

たぶんできている.

実装

#![feature(asm, lang_items, unwind_attributes)]
#![no_std]
#![no_main]

extern crate avr_delay;
extern crate avr_std_stub;
extern crate avrd;

use avr_delay::delay;

use avrd::attiny13a::{DDRB, PORTB};

#[no_mangle]
pub extern "C" fn main() {
    unsafe {
        core::ptr::write_volatile(DDRB, 0x08);
    }
    let mut out = 0x08;
    loop {
        unsafe {
            core::ptr::write_volatile(PORTB, out);
        }
        delay(1_200_00);
        out ^= 0xFF;
    }
}

書き込み

f:id:symrio:20200924003410j:plain

動作確認

f:id:symrio:20200924003716g:plain

所感

参考

AVRDUDE関連

Atmega328p FT232RL invalid device identifier FT232RLでAVRライターを自作してATtiny85をDigispark互換にするまで - Qiita kemarin-tech : avrdudeでAVRにバイナリコードを書き込む(Arduinoボードで書き込み)

AtCoder ABC177

今回はどこでバグっているのかわからなくて,延々と時間を浪費してしまった.

A

use proconio::input;

#[allow(non_snake_case)]
fn main() {
    input! {
        D: usize,
        T: usize,
        S: usize,
    }

    if (D + S - 1) / S > T {
        println!("No");
    } else {
        println!("Yes");
    }
}

B

use proconio::input;
use proconio::marker::Chars;
use std::cmp;

#[allow(non_snake_case)]
fn main() {
    input! {
        S: Chars,
        T: Chars,
    }

    let mut ans = 1001;
    let sl = S.len();
    let tl = T.len();
    for i in 0..=(sl - tl) {
        let mut cnt = tl;
        for j in 0..tl {
            if S[i + j] == T[j] {
                cnt -= 1;
            }
        }
        ans = cmp::min(ans, cnt);
    }

    println!("{}", ans);
}

C

use proconio::input;

#[allow(non_snake_case)]
fn main() {
    input! {
        N: usize,
        A: [u64; N]
    }

    let M: u64 = 1_000_000_007;

    let mut ans = 0;
    let mut cum = 0;
    for a in A.iter().rev() {
        ans = (ans % M + (a * cum) % M) % M;
        cum = (cum % M + a % M) % M;
    }

    println!("{}", ans % M);
}

D

こいつの答えが合わなくて時間を浪費してしまった. 結局の所原因は, union-findをしっかり理解していないところにある.

use proconio::input;
use proconio::marker::Usize1;

fn find(x: usize, par: &mut Vec<usize>, rnk: &Vec<usize>) -> usize {
    //
    if par[x] == x {
        return x;
    } else {
        par[x] = find(par[par[x]], par, rnk);
        return par[x];
    }
}

fn unite(x: usize, y: usize, par: &mut Vec<usize>, rnk: &mut Vec<usize>) {
    let x = find(x, par, rnk);
    let y = find(y, par, rnk);
    if x == y {
        return;
    }

    if rnk[x] < rnk[y] {
        par[x] = y;
    } else {
        par[y] = x;
        if rnk[x] == rnk[y] {
            rnk[x] += 1;
        }
    }
}

#[allow(non_snake_case)]
fn main() {
    input! {
        N: usize,
        M: usize,
        AB: [(Usize1, Usize1); M]
    }

    let mut par: Vec<_> = (0..N).collect();
    let mut rnk = vec![0; N];

    for &(a, b) in AB.iter() {
        unite(a, b, &mut par, &mut rnk);
    }

    for i in 0..N {
        find(i, &mut par, &mut rnk);
    }
    let mut cnt = vec![0; N];
    for i in 0..N {
        cnt[par[i]] += 1;
    }

    println!("{}", cnt.iter().max().unwrap());
}

タクトスイッチ12個を3ピンで制御する

tl; dr.

  • シフトレジスタを使用してボタン入力のピン数を削減
  • 必要ピン数(12本)では8bitシフトレジスタでは足りないので, 2つ繋げて使用
  • 最終的に出力1ピン, 入力2ピンで構成(正確にはこれにVccとGNDが加えられ5ピン)

動機

せっかくArduinoを購入したのだから, 電子工作でシンセサイザー的な何かを作ろうと思った.
しかし, どう考えたってピンが足りないのでシフトレジスタを使用してピン数を節約した.

使用部品

あと実際の完成品には電源確認用のLEDを1つ付けてある.

構成

執筆中

回路

f:id:symrio:20200831001225p:plain
回路図

完成図

f:id:symrio:20200831001352j:plain
完成品

コード

執筆中

動作確認

執筆中

雑記

載せてはいないが今回は配線が非常にキレイにできたので満足している. ただ毎回のようにハンダ付場所をミスるのでどうにかしてほしい.

自作 4桁7セグメント LED 表示器を I2C で制御

[Unfinished]

f:id:symrio:20200827010749j:plain
ブレッドボード上での試作
f:id:symrio:20200827010752j:plain
完成品

(ところで, 試作時にあったデカップリングコンデンサはどこへいった?)

avrdudeがlinuxでattiny85や13へ書き込めなかったの謎