Terrarium

いわゆる掃き溜めの ありふれた有象無象

iPhoneXはAppleのマルチタスキングに対する回答かもしれない

iPhoneX

発売してから半年以上が経過したiPhoneXですが,私も手に入れてから半年近く愛用しています.

そもそもこの機種にしたのは画面が大きい方が電子書籍とか動画とか見やすいだろうという理由でした. 電車で大学に通学する途中でもちろんkindleなどを開いて電子書籍を読むのですが,実際読みやすいです. 前の機種がiPhone5Sというのもあり,画面サイズの大型化は思った以上に良かったです.

そんな中で,ふとiPhoneXを使った効率の良い電子書籍の読み方に気が付いたので,ここに書いてみようと思います.

TL;DR

メモとkindle(などのリーダ)を高速に行き来できるため,本を読みながらのメモが非常に捗る.

(以降は若干ポエム気味です)

iPhoneXにしかない機能

iPhoneXにしかない機能といえば何でしょうか. FaceID?デュアルカメラ?

Appleは(私の知る限り)新商品には必ず新しいインタラクションを備えています. iPhoneXで変わったことといえば,やはりホームボタンの消失が一番大きかったと思っています. ベゼルレスを実現するのに必要だったとはいえ,これまで操作が大きく変わるきっかけになりました.

ホームボタンを押す代わりに,画面の下端からスワイプする. アプリを切り替えるときには,画面の下端を横にスワイプする.

新しいインタクションでしたが,実際に使うとすぐに慣れました.

画面の下端をスワイプするだけで,kindleとメモを行き来できる

この「アプリを切り替えるときには,画面の下端を横にスワイプする」という操作ですが,これが画期的な操作感を生み出していると考えています. これまでの機種(Android含め)はアプリ間の切り替えにホームボタンのダブルタップやアプリ切り替えボタンのタップが求められており,一つの操作では切り替えられませんでした(Galaxyのように2画面同時表示などで解決した例もありますが…iPadも指4本スワイプで切り替えることはできます). それがこのiPhoneXでは,「親指1本で横にスワイプする」だけでアプリの切り替えを行うことができます.

本を読みながらメモをとるというのはとても有効な読み方ですが,電車の中で行うには紙の本では難しく,タブレットはサイズが大きい,重いといったデメリットがありました. このインタラクションを用いることで,iPhoneXでは「メモとkindleをそれぞれ開き,横スワイプで切り替えながら,メモを取りつつ読む」が可能になります. 実際この手法を採用してからは本を読む効率が格段に上がりました. 電車の中だけでなく,右手が空いていれば本をメモ取りながら読めるので.

マルチタスキングのUI

Galaxyの2画面同時表示やiPadの指4本スワイプなど,様々なモバイルデバイスによるマルチタスキング*1インターフェースが考案されてきましたが,私はiPhoneXのインターフェースが一番使いやすいのではないかと考えています. 画面が小さいので,2つのアプリを同時に表示するのはとても見にくくなります. この画面の小ささという制約の中で,簡易な操作によるマルチタスクを実現したインタフェース. 正直iPadにも欲しいです.

狭い画面の中で複数のアプリを同時使用するのに必要なインターフェースという問いに対するAppleの答えが,iPhoneXによるベゼルレスを生かした横スワイプ切り替えなのではないかと思いました.

*1:ここでは,2つのアプリを直列的に同時使用することをマルチタスクと考えています.なのでアプリを逐次切り替えながら使用することもマルチタスクと考えます

今日更新されたYouTube動画を(ほぼ)自動でiPhoneにダウンロードしたい

通勤通学中にYouTube動画をオフラインで見たい

私は家から大学まで電車で通っているのですが,特に帰りの電車ではYouTubeの動画を見て時間を潰すことが日課になっています. しかし毎日YouTubeの動画を見ていてはパケ死してしまいます. そこである時から帰る前にあらかじめ動画をiPhoneにダウンロードしておくようになりました. ただその作業を毎日やるのは面倒なため,なるべく自動化できないか,と試行錯誤した記事です.

iPhoneに動画をダウンロードする

YouTubeから動画をダウンロードするのはどうにかなるのですが,iPhoneに動画をダウンロードする方法もいくつかあり,それぞれ試してみました.

  1. DropboxやGoogleDriveなどのクラウドストレージ経由でオンラインにアップし,iPhoneの対応するアプリからオフライン利用できるように設定する.
  2. AirDrop経由でiPhoneに転送する.
  3. iTunesから動画をライブラリに読み込み,iPhoneをUSB接続して同期する.

それぞれ試してみた結果は次の通りです.

  1. クラウドストレージ経由:PCとiPhoneの両方の操作が必要.同期が非常に遅い.
  2. AirDrop経由:以前は良かったのだがHighSierraにアップグレードしたあたりからAirDropで動画が送れなくなった…
  3. iTunes経由:PCの作業のみではあるが,iPhoneをPCに接続し,ライブラリに追加,同期ボタンを押す手間がある.

実用性を考えると,3番のiTunes経由による同期が一番良さそうに思いました. USB接続する面倒さはありますが,充電ついでということで.

ただこれを毎日やるのは手間がかかる

自分はスプラトゥーンの実況動画をよくみているのですが,毎日更新されるものが多いです. なので動画のダウンロードは意外と手間がかかります. 手順は次の通りです.

  1. YouTubeの登録チャンネルのうち今日更新された動画を全て新しいタブで開く.
  2. 動画ページのURLをコピーする.
  3. ターミナルを開き,youtube-dlを用いて動画をダウンロードする.
  4. 2, 3を全ての新しい動画に対して繰り返す.
  5. iTunesを開き,「ライブラリに追加」からダウンロードした動画を全て選択し追加する.
  6. iPhoneをつなぎ,同期ボタンを押す.

特に1,2,3,4の手間が大きいと感じました. そこでなんとか自動化できないかと試行錯誤した結果を次に述べます.

(なるべく)自動化する

今回は動画のダウンロードにyoutube-dlを使っているので,シェルスクリプトを使えば自動化はできそうです. 最初のポイントは,「今日アップロードされたスプラトゥーンの動画のみ」のURLをどう取得するか.

今日アップロードされた動画のURLを取得する

これはYouTubeAPIを使えばできそうです. 調べてみると,動画の検索を行うサンプルスクリプトが見つかりました. 以下URLのPythonによるサンプルの部分です.

Search: list  |  YouTube Data API (v3)  |  Google Developers

調べると日付やキーワード,チャンネルを指定して検索でき,対応するビデオIDとタイトルを取得できるそうです. ということは,見たいチャンネルIDをあらかじめ持っておけば,キーワードとして「スプラトゥーン」,日付にその日の0時を指定すれば,その時点までに更新された動画のIDが取得できるはず.

次にチャンネルIDを取得したいのですが,よくわからず色々試してみると,どうやらチャンネルトップのURL内の文字列がIDみたいです. https://www.youtube.com/channel/UCXqocGp-RQ_sTw8EpPDgxxxならUCXqocGp-RQ_sTw8EpPDgxxxというわけです. これでチャンネルIDがわかったので,動作確認をしてみました. DeveloperKEYはGoogleのページから取得できます.

承認の認証情報を取得する  |  YouTube Data API (v3)  |  Google Developers

from apiclient.discovery import build
from apiclient.errors import HttpError
from oauth2client.tools import argparser

from datetime import datetime
datetime.now().strftime("%Y/%m/%d %H:%M:%S")


# Set DEVELOPER_KEY to the API key value from the APIs & auth > Registered apps
# tab of
#   https://cloud.google.com/console
# Please ensure that you have enabled the YouTube Data API for your project.
DEVELOPER_KEY = "xxxx"
YOUTUBE_API_SERVICE_NAME = "youtube"
YOUTUBE_API_VERSION = "v3"

def youtube_search(options):
    print("options.q", options.q)
    print("options", options)
    youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
            developerKey=DEVELOPER_KEY)

    # Call the search.list method to retrieve results matching the specified
    # query term.
    search_response = youtube.search().list(
        q=options.q,
        part="id,snippet",
        maxResults=options.max_results,
        channelId=options.channel_id,
        publishedAfter=options.published_after
        ).execute()

    videos = []
    channels = []
    playlists = []

# Add each result to the appropriate list, and then display the lists of
# matching videos, channels, and playlists.
    for search_result in search_response.get("items", []):
        if search_result["id"]["kind"] == "youtube#video":
            videos.append("%s (%s)" % (search_result["snippet"]["title"],
                search_result["id"]["videoId"]))
        elif search_result["id"]["kind"] == "youtube#channel":
            channels.append("%s (%s)" % (search_result["snippet"]["title"],
                search_result["id"]["channelId"]))
        elif search_result["id"]["kind"] == "youtube#playlist":
            playlists.append("%s (%s)" % (search_result["snippet"]["title"],
                search_result["id"]["playlistId"]))

    print("Videos:\n", "\n".join(videos), "\n")
    print("Channels:\n", "\n".join(channels), "\n")
    print("Playlists:\n", "\n".join(playlists), "\n")


if __name__ == "__main__":
    channel_id= "UC8VYesWbdGT6kP4B6vju-rQ"
    date = datetime.now().strftime("%Y-%m-%dT00:00:00Z")
    argparser.add_argument("--q", help="Search term", default="")
    argparser.add_argument("--max-results", help="Max results", default=25)
    argparser.add_argument("--channel-id", help="Channel ID", default=channel_id)
    argparser.add_argument("--published-after", help="Published After", default=date)
    args = argparser.parse_args()
    print("args", args)

    try:
        youtube_search(args)
    except HttpError as e:
        print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content))

これで最新の動画のビデオIDの取得ができたので,次にyoutube-dlを利用するためにその動画のURLを取得する必要があります. 動画のURLはhttps://www.youtube.com/watch?v=DIQBLtuA3sgのようになっているのですが,よくみると最後の文字列がIDっぽいです. 実際に取得したIDを使ったURLでアクセスできたので,これで間違ってないみたいです.

以上より,YouTubeAPIを用いて最新の動画のURLの取得ができました. 次に動画のダウンロードを行います.

youtube-dlをPythonから利用する

動画のダウンロードのために,Python経由でyoutube-dlを呼び出す必要があります. 普段はターミナルから

youtube-dl -f mp4 "https://www.youtube.com/watch?v=ビデオID"

と打てば実行できますが,APIPython経由で叩いているため,このコマンドもPython経由で実行します. そのためには,Pythonのsubprocessモジュールを使います.

17.5. subprocess — サブプロセス管理 — Python 3.6.4 ドキュメント

import subprocess

...

url = "動画のURL"
result = subprocess.check_output(["youtube-dl", "-f", "mp4", url])
print(result)

check_outputメソッドは,引数に取ったコマンドを実行しその出力を返してくれます. なので,結果であるresultを受け取りprint(result)することで結果も表示できます.

以上で動画のダウンロードも自動化できます. 最後に,1番の壁であるiPhoneにどう移すかを検討します.

iPhoneに動画を転送する

iPhoneに動画を転送するにはiTunesAirDropなどmacOS自身に深く関わっている機能を使う必要があり,自動化できなさそうです. AppleScriptなどを使えばできる…?のですが底が深そうなのでやめておきます. なので,できる限り手間を省くにはどうすればいいかを考えました.

と言ってもiTunesを使ったことがある方なら「iTunesに自動的に追加」なるフォルダの存在をご存知の方は多いと思います. そうです,このフォルダに音楽や動画ファイルを入れておけば,勝手にiTunesのライブラリに追加されます. すなわち,ターミナルで次のコマンドを実行すればいいわけです.

mv *.mp4 ~/Music/iTunes/iTunes\ Media/Automatically\ Add\ to\ iTunes.localized

これをsubprocessでやってみようと思ったのですが,これでは*.mp4のようなワイルドカード表現が使えないことがわかりました.

stackoverflow.com

shell=Trueオプションを使えばできるらしいのですがセキュリティ上よくなさそうなので,Pythonのファイル管理モジュールを使うことにしました. ここで登場するのが,パスを管理するpathlibとshellの機能を使えるshutilです.

pathlibにはファイルを検索できるメソッドがあり,shutilにはファイルを移動させるメソッドがあります. この2つを組み合わせると,次のようなPyhtonコードで動画ファイルの移動ができます.

11.1. pathlib — オブジェクト指向のファイルシステムパス — Python 3.6.4 ドキュメント

11.10. shutil — 高水準のファイル操作 — Python 3.6.4 ドキュメント

path = "動画をダウンロードした絶対パス"
_path = pathlib.Path(path)

# globメソッドでmp4形式のファイルを検索しそのパスのリストを取得
video_paths = list(_path.glob("*.mp4"))

dist_path = pathlib.Path("/Users/ユーザー名/Music/iTunes/iTunes Media/Automatically Add to iTunes.localized")

for video_path in video_paths:
    # 動画のパスをdist_pathへmove
    shutil.move(str(video_path), str(dist_path))

実行してみると動画がきちんと移されることが確認できました.

まとめ

以上のスクリプトを使うと,6つだった手順が3つに削減されます.

  1. スクリプトを実行する.
  2. iTunesを開き,動画がライブラリに追加されていることを確認する.
  3. iPhoneをつなぎ,同期ボタンを押す.

また,今回作成したスクリプトGitHubに公開しています.

github.com

スクリプトで自動化したおかげで,動画を探す手間が省けました. ダウンロードの並列化(効果あるのかは知らない)などが今後の課題ですね. このようなライフハック系のコーディングは楽しいので,今後も色々試してみる予定です.

PlatformIOでArduinoの純正IDEから卒業する

PlatformIO

久々にArduinoを使うことになり,せっかくだしコマンドラインから開発したいなぁとなって調べているうちに,良いものを見つけました.

platformio.org

VSCode拡張機能としてインストールでき,ArduinoIDEが無くても動作する優れモノです. 今回はPlatformIOの導入方法について説明してみたいと思います.

Arduinoの純正IDE

(もしかしたら私が知らないだけの機能があるかもしれないです) よくプログラミングをする人はわかると思いますが,Arduinoの純正IDEは機能がとても少ない… 複数のファイル編集もタブしかなく貧弱ですし,変数や関数の補完もしてくれない. Vimキーバインドも使えないし…

いわゆるVSCodeみたいなインテリジェントなエディタに対して機能が貧弱なんですね.

PlatformIO

そこで開発されているのがPlatformIOで,コマンドラインから使えるマイコン向けのビルド・書き込みツールです. Arduinoだけでなく色々なマイコンに対応しているそう.

platformio.org

コマンドラインから使える,と言うことはコーディングと書き込みで別のツールとして使うこともできます. スクリプトを書いて自動で書き込みとかもできる. しかし,ボードの正式名称を調べたり決められたコマンドを使わなければいけなかったりと設定ファイルを作るのがちょっと面倒だったりする.

コマンドを覚えるのが面倒,そう言う人のために,PlatformIOを用いたIDE,PlatformIO IDEと言うものが用意されています.

platformio.org

また新しいIDEを入れなくてはいけないのか…と思いきや,ページを開けばわかりますが,AtomだったりVisualStudioCodeの拡張機能として導入できます(と言うより拡張機能がインストールされたAtomがダウンロードできるといった様子です).

VisualStudioCodeの拡張機能として導入するメリットは以下の通り.

  • 普段使い慣れたエディタで開発できる
  • GUIベースでプロジェクトの設定が可能
    • ボードの選択をはじめとしたプロジェクトの設定をビジュアルに行うことができる
  • ビルドや書き込みもワンボタン
    • PlatformIOの拡張機能をインストールすると左下に専用のボタンが現れ,ビルドや書き込みをワンボタンで行うことができる

VisualStudioCodeにPlatformIOを導入する

VisualStudioCodeは軽量かつ高機能なエディタとして誰にでもお勧めできます. インストールはこちらから.

code.visualstudio.com

何となくやればインストールは終わるはず.

次に拡張機能をインストールします.

f:id:tellusium:20180212133349p:plain

左側のアイコンのうち一番下をクリックし,検索窓にplatformまで入れれば一番上に出てきます(画像ではすでにインストールされていますがインストールボタンが出てくるはず). 基本それだけ.

インストールできるとPlatformIO Homeといったウィンドウが出てくるはず…または左下の家のマークをしたボタンからも開けます. f:id:tellusium:20180212133604p:plain

開いた時の画像. f:id:tellusium:20180212133754p:plain

NewProjectと言うボタンがあるはずなので,それをクリックしボードなどを選びます. ボード選択の部分は文字を入力でき絞り込みが行えます. フォルダの配置場所はご自由に. f:id:tellusium:20180212133903p:plain

プロジェクトが作成されると,フォルダ階層が表示されるはず. プログラム本体はsrc/main.cppにあるので,これを開き,Lチカのコードを書いてみます.

f:id:tellusium:20180212134021p:plain

あとは左下にある「→」ボタンをクリックすると,ビルドと書き込みが行われます.

その他の機能

Arduinoを使う上でシリアルモニタの使いやすさは捨てがたい. もちろんPlatformIOにもその機能が搭載されています. 左下にあるアイコンのうち右のほうにある,コンセントみたいなアイコンをクリックするとシリアルモニタがターミナル上に表示されます. ちゃんと動く.便利.

f:id:tellusium:20180212135050p:plain

まとめ

以上,VisualStudioCodeでPlatformIOを導入するメリットとその方法について説明してきました. 使い慣れたIDEで開発できる,しかもGUIでプロジェクトの設定やビルド・書き込みができるところが非常にありがたい. さらにシリアルモニタまで使えるとか,純正のIDEをはるかに超えた使いやすさでした.

注意

以前platformioを使おうとした時に,PythonのVer2を使えるようにしなくてはならず,PyenvでVer3を使っていた自分にとっては使いづらかった記憶があります(今は消しましたが…). 同様にPyenvを使っていてVer3をデフォルトにしている人は何かしら上記の手順でエラーが出るかもしれません. 今どのバージョンに対応しているのかがぱっと見わからなかったので,もしかしたらすでにVer3に対応しているのかもしれません.

2値画像中のある円領域内の画素数をカウントしたい

コーディング中…

既存論文の実装のために,2値画像中のある円領域内の画素数をカウントする必要が出てきた. 円領域…ということで愚直に実装してみたところ

# imgが画像(numpy array)
# p_x, p_yが円を囲む正方形領域の左上の点
# lengthが円領域の直径
c_x = length / 2 + p_x
c_y = length / 2 + p_y
radius = length / 2

count = 0
for x, y in itertools.product(range(p_x, p_x+length), range(p_y, p_y+length)):
    if ((x - c_x)**2 + (y - c_y)**2 <= radius**2) and img[x][y] > 0:
        count += 1

… 終わらない… 5分経っても終わらない…

調べてみた

こうなることはある程度予測できていたので調べてみた. すると次のStackOverflowのページがヒットした.

stackoverflow.com

np.ogridというものを使っているらしいが時間がないのであとで調べようということで,次のように実装し直した. createCircularMaskTrueFalseによるマスクを返してくれるので,正方形領域であるwindowを抽出したあとでそのマスクをかける. あとはただのnp.arrayなのでnp.count_nonzeroを実行すればカウントできる.

def createCircularMask(h, w, center=None, radius=None):
    if center is None: # use the middle of the image
        center = [int(w/2), int(h/2)]
    if radius is None: # use the smallest distance between the center and image walls
        radius = min(center[0], center[1], w-center[0], h-center[1])
    Y, X = np.ogrid[:h, :w]
    dist_from_center = np.sqrt((X - center[0])**2 + (Y-center[1])**2)

    mask = dist_from_center <= radius
    return mask

c_x = length / 2
c_y = length / 2
radius = length / 2
count = 0

window = img[p_x: p_x+length, p_y: p_y+length]
window = window[createCircularMask(length, length)]
count = np.count_nonzero(window)

すぐに結果が返ってきた. やはりnumpyは偉大ですね.