Google Knowledge Graf API を CLI で叩く
Clapの使い方 備忘録
rustの cli parser である clap
がバージョン3.0
に到達した。3.0
からはstructopt
のようなstruct
を用いたDerive APIが追加された。
使い方を忘れがちなので記録しておく。
基本的な使い方
clap::Parser
を継承した構造体を定義し、parse
でパースすることができる。
またDoc Stringを書くことで引数などのヘルプメッセージを追加することができる。
(Doc Stringを使用しなくても属性で追加することができる。)
use clap::Parser; /// 全体のためのDoc String #[derive(Parser)] struct Args { /// 引数のための Doc String argument: String, } fn main() { Args::parse(); }
オプションの書き方
位置引数
属性を何も指定しないと位置引数として使用することができる。
#[derive(Parser)] struct Args { /// 位置引数 argument: String, }
オプション引数
属性にclap(long)
を指定すると長いオプションを、clap(short)
を指定すると短いオプションを使用することができる。ただし型をOption
で包んでいないと必須オプションとなる。
#[derive(Parser)] struct Args { /// Optional引数 #[clap(long, short)] option: String, }
Option<T>
型に指定することでその引数を任意にすることができる。
#[derive(Parser)] struct Args { /// オプション引数 #[clap(long, short)] option: Option<String>, }
選択引数
clap::ArgEnum
を継承したenum
を型に指定し、clap(arg_enum)
属性を追加することで選択引数を使用することができる。
#[derive(Parser)] struct Args { /// 選択肢引数 #[clap(arg_enum)] selection: Selection, } #[derive(Copy, Clone, ArgEnum)] enum Selection { Type1, Type2, }
フラグ引数
型をbool
にすることによってフラグ引数を使用することができる。
#[derive(Parser)] struct Args { /// フラグ引数 #[clap(long)] flag: bool, }
short
属性についての注意
short
属性は値を指定しないとデフォルトでオプション名の頭文字が使用されるので、頭文字が重複すると実行時にpanic
する。
例えば以下のような引数の設定にするとpanic
する。
#[derive(Parser)] struct Args { #[clap(short)] option1: String, #[clap(short)] option2: String, }
AtCoder ABC230 A-E Rust
AtCoder ABC230 A-E Rust
A - AtCoder Quiz 3
42以上なら1追加する問題.
rustの文字列フォーマットは{:03}
みたいな感じで書く. 毎回忘れる.
use proconio::{fastout, input}; #[allow(non_snake_case)] #[fastout] fn main() { input! { mut N: usize, } if N >= 42 { N += 1; } println!("AGC{:03}", N); }
B - Triple Metre
文字列Sが文字列Tの部分文字列であるか判定する問題. Tはoxx
が[tex: 105]個繰り返すとあるが, 実際には4回程度で良い.(Sが10文字以下であるため)その中から部分文字列を全探索.
use proconio::{fastout, input}; const YES: &str = "Yes"; const NO: &str = "No"; #[allow(non_snake_case)] #[fastout] fn main() { input! { S: String, } let T = "oxxoxxoxxoxx"; for i in 0..3 { if T[i..(i + S.len())] == S[..] { println!("{}", YES); return; } } println!("{}", NO); }
C - X drawin
条件を整理して, 塗る問題. 条件を整理できなかったので解けなかった. 悲しい.
実際は図に起こすとよくわかる.
use proconio::{fastout, input}; const YES: &str = "Yes"; const NO: &str = "No"; #[allow(non_snake_case)] #[fastout] fn main() { input! { S: String, } let T = "oxxoxxoxxoxx"; for i in 0..3 { if T[i..(i + S.len())] == S[..] { println!("{}", YES); return; } } println!("{}", NO); }
D - Destroyer Takahashi
普通に区間スケジューリング問題. 右端を幅Dで調整することに注意.
use proconio::{fastout, input}; #[allow(non_snake_case)] #[fastout] fn main() { input! { N: usize, D: usize, mut LR: [(usize, usize); N] } LR.sort_unstable_by_key(|(_, r)| *r); let mut ans = 0; let mut x = 0; for &(l, r) in LR.iter() { if x < l { ans += 1; x = r + D - 1; } } println!("{}", ans); }
AtCoder ABC232 A-E Rust
AtCoder ABC232 A-E
Rustで解いたときのメモなど
A - QQ solver
文字列をパースして掛け算する問題.
入力として受け取る型がString
とVec<char>
とで後の処理方法が変わる.
Vec<char>
なら文字x
に対してx as u8 - b'0'
で数値化.
use proconio::{fastout, input, marker::Chars}; #[allow(non_snake_case)] #[fastout] fn main() { input! { S: Chars, } let a = S[0] as u8 - b'0'; let b = S[2] as u8 - b'0'; let ans = a * b; println!("{}", ans); }
B - Caesar Cipher
文字列Sをシフトしたとき文字列Tと一致するかどうかを判定する.
文字列Sと文字列Tの先頭の文字のシフト量を計算して, 他の文字のペアのシフト量がすべて先頭のものと一致しているかを判定することで解いた.
文字列S, 文字列Tのiter
をzip
してすべての文字が条件を満たすかall
で判定.
use proconio::{fastout, input, marker::Chars}; const YES: &str = "Yes"; const NO: &str = "No"; #[allow(non_snake_case)] #[fastout] fn main() { input! { S: Chars, T: Chars } let S: Vec<_> = S.iter().map(|&x| x as i32).collect(); let T: Vec<_> = T.iter().map(|&x| x as i32).collect(); let d = (S[0] - T[0] + 26) % 26; let ans = if S .iter() .zip(T.iter()) .all(|(&x, &y)| (y + d) % 26 == x % 26) { YES } else { NO }; println!("{}", ans); }
C - Graph Isomorphism
与えられた2つのグラフ構造が一致しているか判定する問題.
判定方法が問題文に与えられているので, 単純にこれを実装する.
rustで順列を作成するならitertools
のpermutations
を使う.
順列作成で, すべてのリンクに対する判定で
なので, 全体で
で間に合うっぽい.
use itertools::Itertools; use proconio::{fastout, input, marker::Usize1}; use std::collections::HashSet; const YES: &str = "Yes"; const NO: &str = "No"; use std::cmp::{max, min}; #[allow(non_snake_case)] #[fastout] fn main() { input! { N: usize, M: usize, AB: [(Usize1, Usize1); M], CD: [(Usize1, Usize1); M] } let CD: HashSet<_> = CD.iter().collect(); for P in (0..N).permutations(N) { let mut f = true; for &(a, b) in AB.iter() { let c = min(P[a], P[b]); let d = max(P[a], P[b]); if !CD.contains(&(c, d)) { f = false; break; } } if f { println!("{}", YES); return; } } println!("{}", NO); }
D - Weak Takahashi
マスのグリッド上の右か下に移動可能なとき, 左上を始点としたときの最長経路長を求める問題.
右か下にしか移動できないので, 始点から移動可能なマスまでに踏むマスの数は一意に定まるので,幅優先で全探索して最大長を出力.
use proconio::{fastout, input, marker::Chars}; use std::cmp::max; use std::collections::HashSet; use std::collections::VecDeque; #[allow(non_snake_case)] #[fastout] fn main() { input! { H: usize, W: usize, C: [Chars; H] } let mut q = VecDeque::new(); q.push_back((0, 0, 1)); let mut v = HashSet::new(); v.insert((0, 0)); let mut ans = 0; while let Some((x, y, z)) = q.pop_front() { ans = max(ans, z); if y + 1 < H && C[y + 1][x] == '.' && !v.contains(&(x, y + 1)) { v.insert((x, y + 1)); q.push_back((x, y + 1, z + 1)); } if x + 1 < W && C[y][x + 1] == '.' && !v.contains(&(x + 1, y)) { v.insert((x + 1, y)); q.push_back((x + 1, y, z + 1)); } } println!("{}", ans); }
E - Rook Path
ルークの移動を数え上げる問題. dpで解く. dpは以下のとおりに定義.
グループとは下記の画像の通り
- 0(グループA): 終了マス
- 1(グループB): 終了マスと同じ行
- 2(グループC): 終了マスと同じ列
- 3(グループD): それ以外
なのでdpの遷移は以下の通り
- A: B,Cからの移動のみ
- B: AからのBの各列へ移動, BからBの他の列への移動, Dからの移動
- C: AからのCの各行へ移動, CからCの他の行への移動, Dからの移動
- D: BからDの各行への移動, CからDの各列への移動, DからDの他の行と列への移動
最終的な解答は dp[K, 0]
.
遷移の条件を時間以内にまとめられませんでした...
use proconio::{fastout, input, marker::Usize1}; const M: usize = 998244353; #[allow(non_snake_case)] #[fastout] fn main() { input! { H: usize, W: usize, K: usize, x1: Usize1, y1: Usize1, x2: Usize1, y2: Usize1, } let mut dp = vec![vec![0; 4]; K + 1]; let i = if x1 == x2 && y1 == y2 { 0 } else if x1 == x2 { 1 } else if y1 == y2 { 2 } else { 3 }; dp[0][i] = 1; for k in 1..=K { dp[k][0] = (dp[k - 1][1] + dp[k - 1][2]) % M; dp[k][1] = (((W - 1) * dp[k - 1][0]) % M + dp[k - 1][3] + ((W - 2) * dp[k - 1][1]) % M) % M; dp[k][2] = (((H - 1) * dp[k - 1][0]) % M + dp[k - 1][3] + ((H - 2) * dp[k - 1][2]) % M) % M; dp[k][3] = (((W - 1) * dp[k - 1][2]) % M + ((H - 1) * dp[k - 1][1]) % M + ((W + H - 4) * dp[k - 1][3]) % M) % M; } println!("{}", dp[K][0]); }
Rust で wake-on-lan
帰宅した瞬間にパソコンの電源はつくべきである.
alexaとかをトリガーにしてraspからiwake-on-lanを発火すれば良い.
wake-on-lanコマンドはapt install wakeonlan
でインストールできるが,
せっかくだからwake-on-lan の仕様を調べてRustで実装したい.
wake-on-lan の仕様
マジックパケット(Magic Packet)と呼ばれるデータをネットワーク上のすべてのデバイスにブロードキャストとして送信することによって目的のデバイスを起動する.
マジックパケットは先頭にFF FF FF FF FF FF
(6 bytes), その後に目的のMAC Address(6 bytes)を12回繰り返した合計 102 bytesのデータで構成される.
目的の MAC Address がEE:EE:EE:00:00:01
であるならば, マジックパケットは
FF FF FF FF FF FF EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01 EE EE EE 00 00 01
となる.
マジックパケットは UDPとしてブロードキャストアドレスのポート7(Echo Protocol), ポート9(Discard Protocol)へ送信する.
コード
use std::iter; use std::net::UdpSocket; fn main() { // MAC Address let mac_addr = "EE:EE:EE:00:00:01"; let mac_addr: Vec<u8> = mac_addr .split(":") .flat_map(|x| hex::decode(x).unwrap()) .collect(); // magic packet let mut packet = vec![0xFFu8; 6]; packet.extend(iter::repeat(mac_addr).take(16).flatten()); // broadcast let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); socket.set_broadcast(true).unwrap(); socket.send_to(&packet, "255.255.255.255:9").unwrap(); }
所感
テストで何度もついたり消えたりするゲーミングPCくんが可愛そうだ.
raspi が wake-on-lan に対応してほしい...
参照/引用
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 を使用させて頂いた.
例では "オライリー"と検索した結果が表示されている.
参考/参照
リアルタイムOCR [メモ]
リアルタイムでOCRを行うデモをstreamlitを使用して作成するメモ
環境
コード
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
ブラウザにアクセス
前処理を何もしないとやはり検出が難しい.
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
)
高精度版と高速版は公式ページからダウンロードしてくるしか無い(と思う)
- 高精度版: https://github.com/tesseract-ocr/tessdata_best
- 通常版: https://github.com/tesseract-ocr/tessdata
- 高速版: https://github.com/tesseract-ocr/tessdata_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 を試す
実際に使ってみる.
今回は入力画像としてこの画像を使用する.
一応簡単にだが処理時間も計測した.
※ 画像を反転している処理が含まれているが, 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
高精度版
コン ピュ ー ン ヒ ュー タ シ ス テム の 理論 と 実装 lang: jpn_best elapsed time: 0.716602087020874
高速版
コン ピュ ー ン ビ ュー タ シ ス テム の 埋 請 と 実装 lang: jpn_fast elapsed time: 0.4119837284088135
- 青枠は検出された文字列の検出枠と中心線
- 緑枠は検出された文字の検出枠
所感
- 文字が水平である
- 2値化しやすい
- 文字の大きさを揃える
この辺に気を使うとそこそこの精度で検出する. (逆にこの辺をガバると全然検出できない)
参考・引用
- OCRを試すために使用: O'Reilly Japan - コンピュータシステムの理論と実装
- PyOCR公式リポジトリ: World / OpenPaperwork / pyocr · GitLab
- tesseract 公式ドキュメント: Tesseract documentation | Tesseract OCR
Pythonでドラゴン曲線
ドラゴン曲線を描く.
実装
線分の始点から終点へ向かうベクトルを回転させて
倍する処理を繰り返すように実装した.
回転させる角度は
と
が交互になるようにする.
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)
雑記
アニメーションは最初APNGで作成したのだが, はてなブログでは 表示されなかったので泣く泣くgifへ変換した.
参考
FastAPIを使用した画像処理APIの実装
概要
画像を入力とし, 画像を返すAPIをFastAPIで実装する. (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); }); });