加具留矢流余

かぐるやるよ

Google Drive APIでファイルをアップロード - Multipart/Related

前回に引き続きGoogleDriveAPIのお話です。 今回はファイルの新規アップロードについてです。

createエンドポイント

ファイルを新規にアップロードするにはcreateエンドポイントを使用します。

https://developers.google.com/drive/api/v3/reference/files/create

このエンドポイントが曲者で、アップロード時に3つのアップロードタイプから一つを選んで明示的に指定する必要があります。

  • media - ファイルのみをアップロード
  • multipart - ファイルとメタデータを同時にアップロード
  • resumable - 複数回のリクエストでファイルとメタデータをアップロード

現実問題としてメタデータ無しでファイルをアップロードしたいという局面は殆ど無いと思われます。Google Driveではフォルダの階層構造をファイルのメタデータで管理しているため、メタデータ抜きでは親ディレクトリの指定すらできません。

ということで多くの場合においてアップロードタイプとしてmultipartかresumableを指定することになるでしょう。複数回リクエストを投げるよりは一回のリクエストで片付けてしまいたいので、自分の中でmultipartを使ってみようということになりました。ところがこのmultipart、RFC 2387というプロトコルに則ってリクエストを作成する必要があります。日本語の文献もほぼ無いということで素直にRFCを流し読みしました。

RFC 2387 The MIME Multipart/Related Content-type

tools.ietf.org

複数の異なるMimeTypeのデータをまとめてアップロードするための「Multipart/Related」というMimeTypeについてまとめてたものです。あまりしっかりと読めているわけではないですが、要点は以下の通りとなっています。

  • Content-TypeでMultipart/Relatedを指定し、同時にBoundary(境界文字列)も指定する。
  • リクエストボディは"--"+Boundaryで区切って、異なるデータのリクエストボディを指定する。
  • リクエストの終わりは"--"+Boundary+"--"で締める

これだけだと抽象的で少し分かりづらいですが、RFCの原本に載っている例がわかりやすいです。

Content-Type: Multipart/Related; boundary=example-1 start="950120.aaCC@XIson.com"; type="Application/X-FixedRecord" start-info="-o ps"

 --example-1
 Content-Type: Application/X-FixedRecord
 Content-ID: <950120.aaCC@XIson.com>

 25
 10
 34
 10
 25
 21
 26
 10
 --example-1
 Content-Type: Application/octet-stream
 Content-Description: The fixed length records
 Content-Transfer-Encoding: base64
 Content-ID: <950120.aaCB@XIson.com>

 T2xkIE1hY0RvbmFsZCBoYWQgYSBmYXJtCkUgSS
 BFIEkgTwpBbmQgb24gaGlzIGZhcm0gaGUgaGFk
 IHNvbWUgZHVja3MKRSBJIEUgSSBPCldpdGggYS
 BxdWFjayBxdWFjayBoZXJlLAphIHF1YWNrIHF1
 YWNrIHRoZXJlLApldmVyeSB3aGVyZSBhIHF1YW
 NrIHF1YWNrCkUgSSBFIEkgTwo=

 --example-1--

HTTP本体のリクエストヘッダにはMultipart/Relatedを指定しています。startなどはどのデータから読み込めばいいかを示す情報のようです(それっぽいことが書いてありましたが良く理解できませんでした)。リクエストボディの各セクションでは、それぞれのデータに対して改めてヘッダを定義しています。

javascriptでの実装

Multipart/Relatedのリクエストボディを生成する関数を作成した。この関数でエンコードした文字列をリクエストボディにして、リクエストヘッダにはMultipart/Relatedを指定すれば

/**
 * 引数で指定したオブジェクトをMultipart/Relatedのリクエストボディに変換
 * 以下のような配列を第一引数として指定する必要がある
 * e.g)
 * let objects = [
 *   { header: ['Content-Type': 'text/plane'], body: '内容1' },
 *   { header: ['Content-Type': 'text/plane'], body: '内容2' },
 * ]
 * 
 * @param {Array} エンコードしたいオブジェクトのリスト 
 * @param {String} 境界文字列
 * @returns {String} エンコード済みの文字列
 */
function encodeMutipart(objects, separator) {
  let encoded = '';
  for (let o of objects) {
    encoded += `--${separator}`;
    encoded += '\n';
    Object.keys(o.headers).forEach((key) => {
      encoded += key;
      encoded += ': ';
      encoded += o.headers[key];
      encoded += '\n';
    });
    encoded += '\n';
    if (typeof o.body === 'object') {
      encoded += JSON.stringify(o.body);
    } else if (typeof o.body === 'string') {
      encoded += o.body;
    }
    encoded += '\n';
  }
  encoded += `--${separator}--`;
  return encoded;
}

mikebird28.hatenablog.jp

Google Drive APIでパスを指定してファイル一覧を取得する

 Google Drive APIで心が折れた

指定したパスのファイルが存在するか知りたいだけだった。Google Drive Api V3のリファレンスを読んでもあまりファイルパスに関する記述が殆ど見当たらない。クラウドストレージサービスのリファレンスでそんなことあるかと思って調べた。

stackoverflow.com

Try to stop thinking in terms of file paths. Google Drive is a flat filesystem, where "parent" is simply an attribute, a bit like a tag. A file can have many parents, and so could be on many paths.
Google Driveはフラットファイルシステムなので、ファイルパスで考えるのはやめましょう。"親フォルダ"は単なるタグのような属性です。ファイルは複数の親フォルダを持つ可能性がありますし、複数のパスを持つ可能性があります。

Flat Filesystemって何や...って調べてみると英語版ウィキペディアの項目に小さくFlat Filesystemという項目がある。単純に階層構造の無いファイルシステムのようだけど何かしらのメリットがあるのだろうか... Amazon S3も同様の仕組みって書いてあるので大規模クラウドストレージ特有の問題に対応しやすいということだろうか。

要するに指定したパスにファイルが存在するかどうか確認したい場合には再帰的にパスをたどっていくしか無さそう。

listエンドポイント

https://developers.google.com/drive/api/v3/reference/files/list
ファイルのリストを取得するためにはlistエンドポイントを仕様する。 パスで指定するのではなく、qfiledsパラメータを駆使して全てのファイルの中から必要なデータをフィルターするイメージ

qパラメータ

取得したファイル一覧をフィルタリングするためのクエリです。サポートしている文法を確認するためには"Search for files"を確認してください。

fieldsパラメータ

レスポンスに含まれるパスとフィールドを指定します。指定されなければレスポンスはこのメソッドのデフォルトの集合を返します。開発の際にはすべてのフィールドを返す記号*を使えますが、必要なフィールドだけを指定した方がパフォーマンスが優れています。

qで取得するファイルをフィルターして、fieldsで取得する項目(ファイル名、更新日等々)の情報を絞り込む。

javascriptで指定したディレクトリのファイル一覧を取得

javascriptで指定したディレクトリのファイル一覧を取得する。予め何らかの方法でGoogleのOauth2トークンを取得しておく必要がある。 __listDirは補助関数で本体はlistDir関数となっている。

const axios = require('axios');
const API_KEY = '---- Your API Key -----';
const MIMETYPE_FOLDER = 'application/vnd.google-apps.folder'

/**
 * GoogleDriveからファイルのリストを取得する(補助関数)
 * 
 * @param {String} token 
 * @param {String} parent - 上位フォルダのID
 * @returns {Object} Google Drive APIからのレスポンス
 */
async function __listFiles(token, parentId) {
  const url = 'https://www.googleapis.com/drive/v3/files';
  const options = {
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: 'application/json',
    },
    params: {
      key: API_KEY,
      fields: 'files(id, name, mimeType)',
      q: `"${parentId}" in parents`,
    },
  };
  let resp = null;
  try {
    resp = await axios.get(url, options);
    return resp.data.files;
  } catch (error) {
    throw error;
  };
}

/**
 * GoogleDriveからファイルのリストを取得する
 * 
 * @param {String} token - OAuth2トークン
 * @param {String} path - ディレクトリのパス
 * @returns {Object} 指定したディレクトリ内のファイル一覧
 */
async function listFiles(token, path) {
  const sepPath = path.split('/');
  let files = null;
  try {
    files = await __listFiles(token, 'root');
  } catch(error) {
    throw error;
  }

  for (let i=0; i < sepPath.length; i++) {
    let nextId = null;
    for (let e of files) {
      if (sepPath[i] === e.name && e.mimeType === MIMETYPE_FOLDER) {
        nextId = e.id;
        break;
      }
    }
    if (nextId) {
      try {
        files = await await __listFiles(token, nextId);
      } catch (error) {
        throw error;
      }
    }
  }
  return files;
}

一個のファイルの情報を確認するためだけに数回のリクエストが必要になる。可能な限り過去のリクエストをキャッシュして通信回数を減らすのがいいのか。

mikebird28.hatenablog.jp

mikebird28.hatenablog.jp

Google Technical Writing Part 1を読み始めた

英語で技術よりの文書が書けるようになりたくて、Google Technical WrtingのPart 1を読み始めた。

developers.google.com

内容は英語技術文書を書くためのエッセンスを詰め込んだものになっている。読んでいてあたりまえじゃん!って思ったが、実際に過去に自分が書いた文章を見ていると全然出来てなくて驚く。

今のところ「Technical Writing One」〜 「Short Sentence」まで読んだので各章のメモ。

Word(単語)

色々なトピックに関して触れていたので箇条書きで。

  • 文章内で単語の一貫性を保つ
  • 単語を明確に定義する
    • よく知られるものであれば単語の定義のリンクを貼る
    • そうでないなら明確に定義する。量が多いなら用語集を作る。
  • 略称を使う場合には初回だけ正式名称(略称)という表現を使う
  • そもそも略称を使うべきか判断する(長さと頻度を判断基準に)
  • 代名詞は混乱を招くので使用を控える
  • 代名詞を使うのであれば明確な場合にのみ使う

メモ

英語だけでなく日本語で書く場合にも使える普遍的な内容だと思う。一貫性、定義、略称は混乱のもとなので普段から気を付けたい。 代名詞は日本語だと英語に比べて使用頻度が低い気がするが気を付けるに越したことはないと思う。

Active Voice(能動態)

能動態と受動態どちらを使うべきかという話。可能な限り能動態を使うべきという結論だった。理由としては

  • 読者は暗黙的に脳内で能動態に変換している。
  • 能動態に比べて読みづらい。能動態のほうが直接的に表現している。
  • 短い
  • 主語が抜ける場合があって明瞭性に欠ける

メモ

なるほどという感じだった。英語力が低くて逃げの受動態を使っていたが、載ってる例などを見ていると能動態のほうが圧倒的にわかりやすい。

日本語の場合だとどうなんだろうと考えてみた。あまり普段から能動態と受動態を意識したことが無いが、やはり能動態で書いた方がわかりやすいことが多いのかなと思う。日本語は能動態、受動態に関わらず主語が抜けることがあるので、そこが注意するべき点ではないだろうか。

Clear Sentences(明快な文章)

あまり読み込めていないが主なトピックは以下の通り。

  • 強い動詞を使わない
  • Thereを使わない
  • あいまいな形容詞・副詞を使わない

強い動詞というのはhappenやoccurなどの普遍的で使いやすい単語を使うのではなくて、triggerやgenerateなどの使われる場面が限られている=情報量が多い動詞を使うべきと理解した。

メモ

強い動詞を使うという主張は理解しているが、語彙力が無くて普遍的な単語しか使えないという悲哀。Thereは長くて退屈だから使うなという主張のようだ。There文は単純に後ろにある文章を前に持ってくるか、主語をYouに変えて文章書き直せば簡単に直せるよという有り難いアドバイスが書いてあるので活用しようと思う。

Short Sentences(短い文章)

短いコードが読みやすいようにドキュメントも短く簡潔にという内容。

  • 1文には一つのアイディアに留める
  • andで繋いで長くなった文章は、リストに切り出すことを考える。
  • 無駄に長い言い回しを避ける。
  • whichとthatの使い分け(関係代名詞)

メモ

言葉にすれば簡単だが実践するのは難易度が高い内容だと思う。気がつけば長文が出来上がっている。この章の内容も日本語でも積極的に使いたい内容だった。特に日本語は無駄に長い言い回しをするのが得意な言語だと思うので、そこは直していきたい。

whichとthatは完全に同じ意味だと思っていたがそうではないらしい。少なくともアメリカでは。whichは無くても文意が通る場合に使い、thatは無いと文意が通らない場合に使うとのこと。豆知識を得てしまった。

まとめ

まだ前半しか読んでないが有意義な内容だった。長くてやってらんねーよ!という方はトピックを訳している記事があるのでそちらだけでも。

qiita.com

vue.js+vue-routerで自作サービスの多言語対応 Part.1

以前作ったWebサービスの多言語対応を行った。 anntはvue-routerで動くSPAだったが、URLに応じて適切な言語で表示するように改修した。

多言語対応の方法

初期段階で多言語対応ってそもそもどうやるんだろうと思いながらWeb検索をしていると、だいたいどこのサイトでも以下の3通りがメジャーですよと教えてくれた。
4 Key SEO Tips for Your Multilingual Website in 2020

  • グローバルドメイン .jpや.frなどの国固有のドメインを購入して、ドメイン名によって言語を切り替える。 カッコいいとは思うがクソほどお金がかかるのでパス。

  • サブドメイン jp.example.comのように新たにサブドメインを作成して、サブドメインによって言語を切り替える。 1つのドメインサブドメインを設定していくだけなので、追加コストは0。 ただ余り見慣れないURLな気がして少し違和感を感じた+ドメイン周りの設定いじるのがめんどくさいという理由でスキップ

  • サブディレクト 冒頭に示した"https://annt.ai/ja"のような形式で、パスの先頭の値によって言語を切り替える。 ルーティングの設定だけで言語を切り替えられるので、楽+追加コスト無しということでこれを採用した。

当初は実装面の簡便さの観点からサブディレクトリ方式を選んだが、それ以外の要素ってどうなんだろうと思って調べてみるとSEO的にもサブディレクトリが良さそう。

www.seoblog.com

In these cases, you should almost always use subfolders. Subdomains lose a lot of power in terms of SEO, which makes a huge different with regards to marketing and search visibility. Moz has done repeated tests over the years and has determined that using subfolders is simply better SEO practice.

ほとんどの場合サブフォルダーを使用する必要があります。サブドメインSEOに関して多くの力を失い、マーケティングと検索の可視性に関して大きな違いをもたらします。Mozは何年にもわたって繰り返しテストを行っており、サブフォルダを使用するほうがSEOの実践として優れていると判断しました。

どれだけ信頼のおける情報かは分からないが、個人での検証のしようが無いので信じてみようと思う。

Vue.jsでの実装

言語の切り替え

Vue I18nという多言語対応用のライブラリが存在している。どうもi18nはinternationalizationの略語として使われているようだ。(国際化と地域化 - Wikipediaを参考:Wikipediaは何でも乗っていて本当にすごい。)

ライブラリを調べてみたは良いが使い方調べるのが面倒臭くなって、結局ライブラリは使わず言語の切り替えは自分で実装した。国際化という意味だと言語以外の要素(日にちの表現など)を本来は対応する必要があるのかなと思ったが、用途的に別に使わないと考えてjsonに記載したテキストデータから現在の言語に応じて適切なデータを取得する仕組みを作った。

イメージは↓みたいな感じで、各ページのcreatedのタイミングでURLに応じて言語を切り替えるための初期化処理(initLocale)を呼び出す。初期化後はmsgプロパティから現在の言語のテキストが取れるような仕組み。どのページでも使うのでmixinにしてある。当然タイトルなども変える必要があるので初期化後に書き換える。

// ページが読み込まれた際の初期化処理
  created() {
    try {
      // messagesは各言語での文章が書かれたjsonデータを読み込んだもの
      this.initLocale(messages);
    } catch {
      // Force to redirect route.
      this.$router.push('/');
    }
    common.SetTitle(this.msg.page_title);
    common.SetDescription(this.msg.page_description);
  },

多言語のテキストデータを格納したjson

肝心な初期化処理が割と長くなったので記載はしないが、URLのパスに応じてスイッチ文で切り替えるだけなので実装は非常に簡単。 あとはルーティングの設定やhreflangなどについても諸々変更したが、長くなったのでまた次回。

mikebird28.hatenablog.jp

mikebird28.hatenablog.jp

pythonで色を連続的に変化させる方法

色をシームレスに変化させたい。虹色みたいに。ただそれだけ。

f:id:theflyingcat28:20200707003420g:plain
色を連続的に変化させるイメージ

と思っても、実は色についてほとんど知識が無いことに気がつく。RGBは分かるが、それを使ってどう変化させればいいかわからない。RGBのRの値だけを変えても色の赤みが薄くなるか濃くなるかだけで、虹色みたいに変化させるのは難しいのではと思った。

ja.wikipedia.org

困ったときのウィキペディア。世の中には皆がよく知るRGB以外にも色を表現する方法はあるようだ。というかペイントとかでお世話になっていた。 RGBは赤・緑・青の原色を混ぜ合わせることで色を表現する方法だが、HSV色空間は、Hue(色相)、Saturation(彩度)、Value(明度)の3つのパラメータで表現する方法だ。ウィキペディアを見て、色を連続的に変化させたい = 色相を変化させたい、とやりたいことを明確に言語化できた。

ウィキペディアを見るとHSVからRGBの変換方法まで記載されていたので、そのままhsvからRGBの変換関数を作成してみた。

def hsv_to_rgb(h, s, v):
    """ Convert hsv color code to rgb color code.
    Naive implementation of Wikipedia method.
    See https://ja.wikipedia.org/wiki/HSV%E8%89%B2%E7%A9%BA%E9%96%93
    Args:
        h (int): Hue 0 ~ 360
        s (int): Saturation 0 ~ 1
        v (int): Value  0 ~ 1
    """

    if s < 0 or 1 < s:
        raise ValueError("Saturation must be between 0 and 1")
    if v < 0 or 1 < v:
        raise ValueError("Value must be between 0 and 1")

    c = v * s
    h_dash = h / 60
    x = c * (1 - abs(h_dash % 2 - 1))
    rgb_dict = {
        0: (c, x, 0),
        1: (x, c, 0),
        2: (0, c, x),
        3: (0, x, c),
        4: (x, 0, c),
        5: (c, 0, x),
    }
    default = (0, 0, 0)
    rgb = [0, 0, 0]
    for i in range(len(rgb)):
        rgb[i] = (v - c + rgb_dict.get(int(h_dash), default)[i]) * 255
    rgb = map(int, rgb)
    return tuple(rgb)

あとはhを0~360までfor文で変化させたものをgifにすればトップの色が変化していく画像ができあがる。

for i in range(60):
    color = hsv_to_rgb(i*6, 1, 1)
    img = Image.new('RGB', (width, width), (0, 0, 0))
    draw = ImageDraw.Draw(img)
    draw.rectangle((0, 0, width, width), fill=color)
    images.append(img)

images[0].save("./color.gif", save_all=True, append_images=images[1:], optimize=False, duration=5, loop=0)

せっかくだからグラデーションにでもすればもっと見目麗しい画像になった気がする。 あとpillowは簡単にgifが出来て便利。(小並感)

BoundingBoxごと画像を回転・反転・リサイズ

物体検出の前処理だったりデータ水増しを使用とすると、どうしても回転、反転、リサイズなどの処理を行う必要が出てくる。 画像だけであればOpenCV使ったりNumpy使えば、サクっとできるがバウンディングボックスだと若干めんどくさい。(特に回転) ライブラリを作る中で実装したので、それぞれの簡単な実装を紹介する。

前提

BoundingBoxの表現として以下のようなBoxクラスを使う。x、yは画像左上の座標、w, hはBoundingBoxの幅と高さを表している。

画像の読み込みにはopencvを使い(height, width, channel)の形をしたnumpy配列として読み込む。デフォルトだとEXIFオリエンテーション情報を持っている場で合にはそれが適用されてしまうので、今回はcv2.IMREAD_IGNORE_ORIENTATIONで画像の回転に関する情報は無視する。

あと便利なので描画用の関数showを用意する。cv2にはデフォルトで透過した矩形を描く機能が無いので自前で用意する必要がある。

import cv2
import numpy as np


class Box:
    def __init__(self, x, y, w, h):
        self.x = x
        self.y = y
        self.w = w
        self.h = h

def show(image, boxes, color, alpha):
    image = image.copy()
    for b in boxes:
        overlay = image.copy()
        p1 = (int(b.x), int(b.y))
        p2 = (int(b.x + b.w), int(b.y + b.h))
        cv2.rectangle(overlay, p1, p2, color, -1)
        image = cv2.addWeighted(image, alpha, overlay, 1-alpha, 0)
    cv2.imshow('img', image)
    cv2.waitKey()

image = cv2.imread(filepath, cv2.IMREAD_IGNORE_ORIENTATION | cv2.IMREAD_COLOR)

回転

今回の3つの操作の中だと一番めんどくさい気がする。画像の回転をAffine変換でやれば、そのときに変換行列を計算しているはずなのでBoundingBoxの座標変換にもそれを流用してしまうのが良い。注意点として4隅の座標を変換すると、変換後のBoundingBoxが辺がx, y座標と平行にならない。そのため変換後の座標から画像の辺と平行になるよう再度x, y座標を計算しなおす必要がある。

def rotate(image, boxes, angle):
    img = image.copy()
    # 初期状態での高さと幅
    h, w = img.shape[:2]

    # sinとcosを駆使して回転後の画像サイズを計算
    r = np.radians(angle)
    s = np.abs(np.sin(r))
    c = np.abs(np.cos(r))

    nw = int(c*w + s*h)
    nh = int(s*w + c*h)

    # 回転行列を計算+サイズの変化に伴う補正を加える
    center = (w/2, h/2)
    rot_m = cv2.getRotationMatrix2D(center, angle, 1.0)
    rot_m[0][2] = rot_m[0][2] + (nw - w) // 2
    rot_m[1][2] = rot_m[1][2] + (nh - h) // 2

    # アフィン変換を実行
    img = cv2.warpAffine(img, rot_m, (nw, nh), flags=cv2.INTER_CUBIC)

    new_boxes = []
    for box in boxes:

        # Boxの情報を座標に
        coord_arr = np.array([
            [box.x, box.y, 1],  # Left-Top
            [box.x, box.y+box.h, 1],  # Left-Bottom
            [box.x+box.w, box.y, 1],  # Right-Top
            [box.x+box.w, box.y+box.h, 1],  # Right-Botto
        ])

        # 回転行列と掛け合わせて座標変換
        new_coord = rot_m.dot(coord_arr.T)
        x_ls = new_coord[0]  # x座標だけを抽出
        y_ls = new_coord[1]  # y座標だけを抽出

        # 最小値最大値を用いて、新しい四隅の位置を計算
        x = int(min(x_ls))
        y = int(min(y_ls))
        w = int(max(x_ls) - x)
        h = int(max(y_ls) - y)
        new_box = Box(x, y, w, h)
        new_boxes.append(new_box)
    return img, new_boxes

反転

画像自体の反転はnumpyのflipudとfliplrで簡単にできる。BoundingBoxの計算も、xの値を左端までの距離から右端までの距離に、yの値を上端までの距離から下端までの距離に計算しなおすだけなので簡単。

def flip(image, boxes, flip_x=True, flip_y=False):
    img = image.copy()
    ih, iw = img.shape[:2]

    # 引数に応じてどの軸で反転させるかを判断
    # デフォルトではx, y軸双方で反転させる
    if flip_x:
        img = np.fliplr(img)
    if flip_y:
        img = np.flipud(img)

    # BoundingBoxの座標を計算
    new_boxes = []
    for box in boxes:
        x = box.x
        y = box.y
        if flip_x:
            # 右端からの距離
            x = iw - box.x - box.w
        if flip_y:
            # 下端からの距離
            y = ih - box.y - box.h
        new_box = Box(x, y, box.w, box.h)
        new_boxes.append(new_box)
    return img, new_boxes

リサイズ

def resize(image, boxes, width, height):
    # 現在の高さと幅を取得しておく
    c_height, c_width = image.shape[:2]
    img = cv2.resize(image, (width, height))

    # 圧縮する比率(rate)を計算
    r_width = width / c_width
    r_height = width / c_height

    # 比率を使ってBoundingBoxの座標を修正
    new_boxes = []
    for box in boxes:
        x = int(box.x * r_width)
        y = int(box.y * r_height)
        w = int(box.w * r_width)
        h = int(box.h * r_height)
        new_box = Box(x, y, w, h)
        new_boxes.append(new_box)
    return img, new_boxes

結果

上の操作をすべて実行するとこんな感じで変換されていく。

image = cv2.imread(filepath, cv2.IMREAD_IGNORE_ORIENTATION | cv2.IMREAD_COLOR)
boxes = [Box(83, 108, 267, 124)]

show(image, boxes, (255, 0, 0), 0.5)

# Rotate
image, boxes = rotate(image, boxes, 90)
show(image, boxes, (255, 0, 0), 0.5)

# Tilt
image, boxes = rotate(image, boxes, 3)
show(image, boxes, (255, 0, 0), 0.5)

# Flip
image, boxes = flip(image, boxes)
show(image, boxes, (255, 0, 0), 0.5)

# Resize
image, boxes = resize(image, boxes, 500, 500)
show(image, boxes, (255, 0, 0), 0.5)

f:id:theflyingcat28:20200626211033p:plainf:id:theflyingcat28:20200626211037p:plainf:id:theflyingcat28:20200626211041p:plainf:id:theflyingcat28:20200626211044p:plainf:id:theflyingcat28:20200626211049p:plain
変換の推移(左から右に変換されていっている)

今回の3操作はannt-python にも実装されている。

mikebird28.hatenablog.jp mikebird28.hatenablog.jp

Dropbox上の画像を直接アノテーションできるサービスを作った。Part 2

前回に引き続きanntというサービスを作った話です。 part.2 では主に技術面の話について書いていきます。

目次

フロントエンドの話

今回始めてvue.jsを使ったのですが本当に便利です。何が便利かってサイトを構成するパーツを分けて実装できるのが本当に楽でした。anntはvue-routerを使ってSPAとして作成したのですが、思ったよりぬるぬる動いてこれは良いものだとなりました。実装していく際にも各ページ毎にComponentとして実装していくので、特にファイルが肥大化することもなく読みやすく作れます。勢いで作ったので一部は肥大化してしまいましたが。

これまでjs周りでjquery以上の何かを使ったことが無かったので、個人用のツール作るときは読みづらい+肥大化する⇒放置というパターンに陥っていたのですが、適当な粒度でパーツ毎に切り出していけるのでダイアログとか作るのはすごい楽でした。

f:id:theflyingcat28:20200621212417p:plain
vueで作ったダイアログ

正直Reactなどの他の仮想DOMライブラリとの違いを把握していませんが、何か新しいWebアプリ作りたいってなったら学習コスト含めてもこの手のライブラリを使っていくのが良いのかなぁと思いました。

個人的に気になるのはこの先どのくらいサポートされるのだろうか...という点です。なんかしょっちゅう新しいライブラリが出てきているイメージがあるので、徐々に廃れていって突如開発中止になるのは怖いなぁと思います。まぁでもこれだけ勢いのあるライブラリであればしばらくは心配する必要も無いのでしょうか。

canvas周りについて

実際にアノテーション作業を行う画面はほとんどcanvasでできています。作業の大半の時間はここを作るために費やされた気がします。大変だったのは座標の変換処理とBoundingBoxを作るところでした。

座標の処理を作っている際は発狂していました。addEventListenerでclickなどのeventの座標を取得⇒canvas上の座標に変換⇒画像上の座標に変換、と言葉にしてしまえば3ステップですが、画像を回転させたりすると変換がどんどん複雑になって沼にハマりました。本当はこの辺の座標変換の処理を上手く適当なクラスに隠蔽して画像上の座標だけを意識して作業できるようにすれば良かったんですが、気がついたら後戻りできないところにいた気がしたので力づくで成し遂げました。リファクタリングは今後の課題です。

BoundingBoxはいつか見た下のリンクに非常に参考になる実装があったので、ありがたく勉強させて頂きました。

hashrock.hatenablog.com

これまでペイントツールとかで良く使っていたもののはずが、いざ実装してみるとBoundingBoxのことを全然理解していないということに気が付きました。特に画面外に持っていこうとしたときの処理とか全然作れなくて、結局動いているのかどうか怪しい実装になってしまった気がします。

DropboxAPI

DropboxAPIを公開しているのでoauthで認可してもらえれば普通に叩けます。ドキュメントがしっかりしているのでそこまで苦労はしませんでしたが、APIによって引数の渡し方が違ったりするとこで少し躓きました。でもドキュメントをしっかり読んでれば回避できたことなので、ドキュメントをしっかり読みましょうという教訓を得ました。あと作りきった後にSDKが存在していることを知りました。下調べをしっかりしましょうという教訓も得ました。ここに関しては反省しかないです。

バックエンドの話

firebaseでjavascriptを使いました。どうしてもクライアント側に持たせたくない処理だけをサーバー側で持つようにしました。なので全部で500行行ってなかったです。firebaseは安いし楽だし便利なのですが、個人的にはKey-Value型のやつがよくわからないのでRDBが使いたかったです。それ以外は満足です。

pythonライブラリの話

特に書くことも無いのですが、こだわったポイントとしてはプロパティを使って, x, y, width, heightという表記でも, left, top, right, bottomという表記でもどちらでも使えるようになっています。widthを書き換えればrightの表記も書き換わるので割と便利だと思います。ぜひ使ってみてください。自分の環境でしかテストが出来ていないので動くかが不安ですが。

画像の読み込みだったり表示だったりはopencvでやっているので全部でこちらも500行くらいでした。READMEだったりsetup.py書く時間の方が長かったです。

まとめ

いろんなライブラリがあって本当に便利な世の中だなぁと思いました。なので作業する前にちゃんと公式のライブラリが無いか探してドキュメントを読む癖をつけましょう。お兄さんとの約束です。作り終わった後に必要だったライブラリ見つけると本当に時間を無駄にした感がすごいです。

mikebird28.hatenablog.jp

mikebird28.hatenablog.jp