Google Knowledge Graf API を CLI で叩く

Google検索したときに右に出てくるアレをCLI上でも表示させたい。

Google Knowledge Search APIとは

実行

f:id:sh1m088io:20220127094851g:plain
Demo

メモ

  • ビルド時にAPI KEYを読み込んでいるので、バイナリを解析するとAPI KEYが読めてしまうのでバイナリを配布しないする
    • 環境変数にAPIKEYを入れたほうが良いのであればそちらに切り替え

参考

Clapの使い方 備忘録

rustの cli parser である clapがバージョン3.0に到達した。3.0からはstructoptのようなstructを用いたDerive APIが追加された。

github.com

使い方を忘れがちなので記録しておく。

基本的な使い方

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

atcoder.jp

github.com

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

条件を整理して, 塗る問題. 条件を整理できなかったので解けなかった. 悲しい.

実際は図に起こすとよくわかる. f:id:sh1m088io:20220105214012j:plain:w300

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で解いたときのメモなど

atcoder.jp

github.com

A - QQ solver

文字列をパースして掛け算する問題.
入力として受け取る型がStringVec<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のiterzipしてすべての文字が条件を満たすか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で順列を作成するならitertoolspermutationsを使う.

順列作成で O(N!), すべてのリンクに対する判定で O(M)なので, 全体で O(N! M)で間に合うっぽい.

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

 H \times Wマスのグリッド上の右か下に移動可能なとき, 左上を始点としたときの最長経路長を求める問題.
右か下にしか移動できないので, 始点から移動可能なマスまでに踏むマスの数は一意に定まるので,幅優先で全探索して最大長を出力.

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は以下のとおりに定義.

 dp(i, j) := i回動かした後のjグループに存在する総数

グループとは下記の画像の通り

  • 0(グループA): 終了マス
  • 1(グループB): 終了マスと同じ行
  • 2(グループC): 終了マスと同じ列
  • 3(グループD): それ以外

f:id:sh1m088io:20220105013217j:plain

なので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 を使用させて頂いた.

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

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

参考資料