2017年 09月 24日

Mac でウェブカメラの定期撮影を Swift で書く

macOS 用にウェブカメラからjpgを取得するコマンドラインツールにimagesnapというのがある。単発で使うには問題ないんだけど、インターバル撮影を数時間続けたところメモリを食いつくして死んだため代替を書く気概になった。

毎回プロセスを立ちあげればメモリリークが起きないが、カメラデバイスを初期化する処理が数秒あって遅いので、できればちゃんとデバイスを開きっぱなしで動かしたい。

やりたいこと

やりたいことはウェブカメラ経由のタイムラプスの撮影で、撮影したファイルはffmpegで読みこませる。そのためファイル名にも要件がある

  • foobar-00001.jpg みたいなファイル名にする
  • メモリリークしない

swift で書きなおす

xcrun swift で動かせるとスクリプト的に使えて便利なので、そのようにした。特に難しいことはしてないんだけど、やりかたをググっても API がのきなみ古いとかでやる気がなくなる。現時点では最新の方法だが、iOS だと既に deprecated になっているAPIが macOS だと生きていたりするのでややこしい。

#!/usr/bin/xcrun swift
import Darwin
import Cocoa
import AVFoundation

struct ProgramOption {
	var deviceName: String? = nil
	var list: Bool = false
	var help: Bool = false
	var interval: Double = 10
	var format: String = "snapshot-%05d.jpg"
	var sequence: Int = 1
}

var option = ProgramOption()
var args = CommandLine.arguments
while (!args.isEmpty) {
	let arg = args.removeFirst()
	switch arg {
		case "-h", "--help":
			option.help = true
		case "-l", "--list":
			option.list = true
		case "-d", "--device":
			let val = args.removeFirst()
			option.deviceName = val
		case "-i", "--interval":
			let val = args.removeFirst()
			option.interval = Double(val)!
		case "-f", "--format":
			let val = args.removeFirst()
			option.format = val
		case "--start":
			let val = args.removeFirst()
			option.sequence = Int(val)!
		default:
			break
	}
}

if option.help {
	print("Usage: $0 [options]")
	print("  -h --help                     show help")
	print("  -l --list                     list devices")
	print("  -d [name] --device [name]     specify camera device name (default: system default device)")
	print("  -i [num] --interval [num]     specify snapshot interval (default: 10)")
	print("  -f [format] --format [format] specify filename format (default: snapshot-%05d.jpg)")
	print("  --start [num]                 specify start number of sequence")
	exit(0)
}

if option.list {
	let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo)! as! [AVCaptureDevice]
	for var device in devices {
		print(String(format: "'%@' (%@ %@)", device.localizedName, device.manufacturer, device.modelID))
	}
	exit(0)
}


print("opening capture device")
let device:  AVCaptureDevice
if option.deviceName != nil {
	let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo)! as! [AVCaptureDevice]

	device = devices.first(where: { (i) in i.localizedName == option.deviceName })!
	print(device)
} else {
	print("using default capture device")
	device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)!
}

let input = try! AVCaptureDeviceInput.init(device: device)
let output = AVCaptureStillImageOutput()

let session = AVCaptureSession()
session.sessionPreset = AVCaptureSessionPresetPhoto
session.addInput(input)
session.addOutput(output)
session.startRunning()

let connection = output.connection(withMediaType: AVMediaTypeVideo)!

let semaphore = DispatchSemaphore(value : 0)
var seq = option.sequence
while (true) {
	output.captureStillImageAsynchronously(
		from: connection,
		completionHandler: { (imageDataBuffer, error) in
			print("captured")
			semaphore.signal()
			if let e = error {
				print(e.localizedDescription)
				return
			}
			if let buffer = imageDataBuffer {
				if let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer) {
					do {
						let filename = String(format: option.format, seq)
						try imageData.write(to: URL.init(fileURLWithPath: filename), options: [])
						seq += 1
						print("wrote to file \(filename)")
					} catch {
						print("failed to write file...")
						exit(1)
					}
				}
			} else {
				print("buffer is empty")
			}
		}
	)

	// output.isCapturingStillImage
	semaphore.wait()

	print("sleep \(option.interval) sec")
	Thread.sleep(forTimeInterval: option.interval)
}

デスク整理用のなにか

デスクを部屋の中に向けて配置しているけど、必然的に部屋の中側にケーブルが煩雑に這うようになるので、これの見た目を多少でも良くしようと覆いをかぶせている。(わかりにくいと思うけど)

これまで微妙な固定方法をとっていたが、3D プリンタでうまい具合の部品を出せた。覆い自体はプラスチックダンボールに木目シートを貼ったもので、軽いので、単にクリップで止められるようにした。2つプリントしてつけてる。

ケーブルをある程度束ねておけるようなスペースもつけた。

M15 なら Fusion360 で直接ネジをモデリングしてプリントしても締めることができた。

Prusa i3 MK2S のベッドにウェブカメラC270をつける

表題のようなものは thingverse とかで検索するといろいろあるんだけど、だいたいベッドを固定しているビスと共締めするものになっている。あんまり共締めしたくなかったのでベッドのフレームを挟みこんで固定するようなものをモデリングした。

3つのパーツで、ベッドに近いところは PETG でプリントしている。

C270 用のピント調節機能つきカバー

https://www.thingiverse.com/thing:714475 をベースにして、ピント調節リングだけhttps://www.thingiverse.com/thing:1406879 に変えたものをプリントした。

製品もともとのピント調節だとどうしてもレンズに指がかぶってしまってあわせるのが難しかったのでめっちゃ便利。

Prusa i3 MK2S のエクストルーダー付近

ダストフィルター・オイラー

https://www.thingiverse.com/thing:2086804

一回エクストルーダーが詰まって定量でなくなったことがあったのでつけてみた。埃をふきとると同時に油をつけている。これをつけてからは一回も詰まってないので効果があるっぽい。

30mm ファンカバー

https://www.thingiverse.com/thing:2523236

これ。0.1mm で出力しないとハニカム部分が薄くなりすぎるので罠

ファンカバーはいらないと思っていたけど、糸状になったプラスチックが噛みこむことがあって、そうするとかなり悪い状況になるので多少でも緩和できるかと思ってつけることにした。

40mm ファンカバー

https://www.thingiverse.com/thing:2076234

これのうち FrontFanCover_RoundV2.STL だけ出力した

レイヤーファンノズル

https://www.thingiverse.com/thing:2276997

デフォルトだと横からノズルに風があたるようになっていて、あまり出力レイヤーには風があたらない。PLA だとできるだけ出力したものを急冷させるのが反りを軽減するのに大事らしいので、効果を期待してかえてみた。PETG で出力。

2017年 09月 19日

Original Prusa i3 MK2S Kit (3Dプリンタ) を買ってた

http://www.prusa3d.com/

購入から届くまで

特に急ぐ買いものではなかったので UPS Expedited で頼んだ。頼んでトラッキングを見るまで US の会社だと思っていたのだけど、チェコの会社で、プラハからの発送だった。ドイツとシェンツェン経由で成田にきて、通関後はヤマト運輸に委託で届いた。消費税が4000円弱ぐらいだった。

組立

トータルで8時間ぐらい。公式組立マニュアルのセクションごとにパーツが袋わけされていたり、部品の実寸ラベルが貼ってあったり、各セクションごとにほとんど過不足なくビスが梱包されていたりとめっちゃ丁寧だった。あと公式マニュアルは手順ごとにコメントが書けるようになってて、これの数で「ハマりそうな手順やな」と気付ける。

簡単に組み立てできるかっていうと簡単ではないよなという感想を持った。工具はついてるが、別途200mmぐらい測れるノギスが必要。普通は持ってないだろう (150mm しかないので買った)。マニュアルも完璧とは思わなくて、最初のほうにつけるナットがなんのためにつけるナットなのかとかの説明がないので、どのぐらいの精度でつければいいかわかりにくいことがある (精度が必要なところは必要と書いてあるので気にしなければいいんだが)

あとリニアベアリングのすべりが単体でシャフトに入れてもいまいちよくなくて不安だったけど、ある程度ひっかかっても大丈夫みたい? 神経質にならなくても脱調するほどではなかった。このへんの肌感覚みたいなのがわからないので、YouTube で組立動画を見たりした。

構造的に M10 と M8 のネジ + プリントパーツで構成される一番下の部分は、剛性があまりなくて歪みがでやすくて難しい。歪みをなおすときは逆の力を加えるというゴリラ的手法なので、組み立てたあともうっかり力をかけると歪みそうなので気をつかう。逆に考えると、組立済みを買うとこのへんの構造に対する理解が不足してトラブルシューティングが難しくなるわけだから、やっぱ自分で組み立てたほうがいいと思う。

初回起動とキャリブレーション

添付の 3D Printing Handbook (オンライン版はドライバページにある http://www.prusa3d.com/drivers/ ) のフローチャートにしたがって初回キャリブレーションを行う。

これもよくできてて、Selftest で各所のパーツが正しく繋がれているか確認してくれる。安心。Calibrate XYZ はかなり時間がかかったが XYZ calibration all right. X/Y axes are slightly skewed. Good job. みたいなことを言われて成功した。X/Y の直角が完全には出ていないみたいだが、多少ならソフトウェアで補正するので良いらしい。直角だすのはかなりくたびれると思うが、多少ずれてても Good Job って言われるのはうれしい。

ここまでやるといよいよテストプリントになるが、キャリブレーション用のパターンをプリントしても、素人目だとさっぱりわからなくてむずい。大きくずれてはいなそうだったので、Prusa ロゴをプリントしながら Z 軸調整した。

キャリブレーション用のパターンは1レイヤーで密にプリントする形式のほうが、隙間のあるなしを判定できるので結果がわかりやすいんじゃないかなあ。

CNCフライスとの比較

やっぱり段取りが少ないのは良い。設計から出力開始までが短くて、小さいものなら1時間ぐらいで終わる。

切削加工だと、ワーク選定・ワーク固定・エンドミル固定・原点出し・空切削と実切削までの手順が多く、特に固定と切削条件出しが難しいので大変。加工できない形状が多いので設計時にもかなり頭を使う。そのぶん精度と信頼性高いし金属加工できるんだけど、そこまでやらなくてもいいような立体を出したいことも結構あるんだよなあと思う。

X/Y のずれをなおす

「XYZ calibration all right. X/Y axes are slightly skewed. Good job. 」なのはいいんだけど、簡単にアライメントなおす方法を思いついたのでやってみた。単に直角定規をX軸のリニアシャフトにあてながらベッドの線と平行を確認するというだけだけど、これで劇的にわかりやすくなった。取説だと上から見ろって書いてあるけど、なんでもいいから板をあてればよさそう。

これでキャリブレーションをやりなおして、「XYZ calibration ok. X/Y axes are perpendicular. Congratulations!」となった。

ちなみにプリントクオリティは見ても特に変わらなかったので、ソフト補正が普通に優秀っぽい。

余談

今年は買うつもりがなかったんだけど、あまりにもひどいことが続くので楽しいことに金を使いまくろうと散財。最大の問題は、家の中に既にこのようなものを置くスペースが全くないことで、現実に今も困っている。デスクの上に置いたままなのでPCがぜんぜん使えない。アホだな!

使ってみた感想はまた後日

2017年 08月 16日

minimist でサブコマンド付きコマンドを実装する

node.js 用のコマンドラインパーサである minimist は必要最低限かつわかりやすくていいですね。

しかもサブコマンド用のオプションも用意されているので、ある程度複雑でも耐えられます。

以下がサブコマンドを実装したもののサンプルコードです。ポイントは親コマンド側では stopEarly を指定して、サブコマンド用の引数を解釈させないところです。

そのうえであらためてサブコマンド用の引数を minimist にかけることで必要な機能が実現できます。良いですね。

#!/usr/bin/env node

import * as minimist from "minimist";

const commands : { [key: string]: (argv: any[]) => void } = {
	foobar : function (_argv: any[]) {
		const argv = minimist(_argv, {
			boolean: true,
			alias: {
				"o": "output",
				"h": "help",
			}
		});
		console.log(argv);
	}
};

const argv = minimist(process.argv.slice(2), {
	stopEarly: true, // do not parse subcommand args
	boolean: true,
	alias: {
		"h": "help",
	}
});

const subcommand = argv._.shift() as string;
if (!subcommand || argv.help) {
	console.log('Options:');
	console.log('  -h --help: show help');
	console.log('Sub commands:');
	process.exit(argv.help ? 0 : 1);
}

const func = commands[subcommand];
if (!func) {
	console.warn(`Unknown subcommand ${subcommand}`);
	process.exit(1);
}

func(argv._);
2017年 08月 07日

KiCAD の回路図ファイルを見るツール (続き)

KiCAD の回路図ファイルを CUI で SVG に変換したり、そのままブラウザで見たりする kicad-utils | tech - 氾濫原 の続き。KiCAD の回路図ファイルをブラウザから直接見れるようにしている。

だいぶよくなってきた。完成度85%ぐらいかな?

  • 色がついた
  • 文字レンダリングを KiCAD 同様にストロークフォントを自力描画するように
    • これをやらないと SVG などでフォントメトリクスを正確に知る方法がないため文字位置がどうしてもずれてしまう
  • ウェブのビューワーでクエリパラメータを受けるようになった
  • ブックマークレット経由で即座にプレビューできるようになった

ブックマークレット

index ページから設定できる。

Open with Schematic Viewer をブックマークに追加した状態で github などにある回路図ファイルが入ったディレクトリ (例: KiwiSDR) で実行するとビューワーでひらける。

未解決の問題

回路図ファイルで、コンポーネントの名前や値を表示するときにずれることがある。

この問題はいまいちよくわからない。元のコードを読んでもかなりややこしいことをしてて、何がしたいコードなのかわからない。.sch を編集するときは、.lib 内で定義したラベル類は再配置可能なので、それのためにコードが入ってるのだが、どこ基準で再配置されているのかよくわからない。

今のところの理解ではコンポーネントの中心とラベルの中心が相対的になるように配置する、だと思ってるんだけど、そういうつもりの実装を書いてもうまくいかない。

普通にボックス計算が間違えていたので修正したらなおったっぽい

.lib ファイルが指定されてないときの挙動

  • ...-cache.lib には使っているコンポーネント類がすべてコピーされていると期待しているが、そうではないことがある。
  • ...-cache.lib を配布してないケースがある (KiCAD のヘルプではこれは配布しなくていいことになっている)

最初の問題はよくわからない。あんまり発生しないけどちゃんと調べてない。-cache.lib の生成タイミングがそもそもわかってない。

.lib ファイルを同梱してない .sch の場合、基本的には忠実に再現することはできない。デフォルトで https://github.com/KiCad/kicad-library の最新を読むようにする、とかはできそうだけど、互換性が保たれている保証はない気がする。

...-cache.lib は基本的には配布物に含めたほうがいいと思うんだけど、どうなんだろう。

2017年 08月 03日

KiCAD の回路図ファイルを CUI で SVG に変換したり、そのままブラウザで見たりする kicad-utils

デモとレポジトリ

回路図ビューワ。開いてすぐ Load from url すると、github で公開されている回路図を読みこんで表示するようになっている。

レポジトリは https://github.com/cho45/kicad-utils

CUI

npx で実行できるようにしてみた。以下のようにすると同名の .svg ファイルを生成する。

npx -p kicad-utils sch2svg path/to/file.sch

経緯

github などに KiCAD の回路図ファイルをアップロードしても実際に閲覧するためにはダウンロードして KiCAD で開くという作業が必要で、これがなかなか面倒くさい。KiCAD は同時に1つしかプロジェクトを開けないので、他の作業をしてるときに別の回路図を開く気が起きない。というかそもそもダウンロードが面倒くさい。

たとえば回路図を複数人で開発するなり、レビューするなりしようと思うとかなり面倒なコンテキストスイッチが発生する。特に github のレビューフローに慣れている人間は github 上で見れないなら見ないので良くない。

他にも理由はあるけどおおむね以上のようなことが主因で、とりあえず CUI で回路図を png や SVG に変換できるツールが欲しいと思った。

実装言語としても検討した結果、使い慣れており応用例が広いブラウザ上で動くことを念頭に TypeScript とした。

実装

最初は KiCAD 自体のファイルを再利用 (emscriptenなど) することを考えたが、全体的に wxWidgets 依存があって無闇にハマりそうと感じた。

その過程で実装を読むと移植してもそれほど工数がかからないのではないかという感じがしたため、TypeScript でフル実装を書く方針とした。

まずコンポーネントライブラリ (.lib) ファイルのパーサーと canvas API を利用するプロッター(レンダラー) を書いた。

次の回路図 (.sch) ファイルのパーサーと対応するプロッターを書いた。この途中で SVG で出力するプロッター実装も書いた。プロッターレイヤーで、canvas 出力と SVG 出力を抽象化していて、これは KiCAD の実装と同様。

というか、できるだけミニマルな実装を書こうと思ったが、そうすると対応が不完全になりがちなのと頭を使いすぎるので、本体変更に比較的追従しやすいようにコードがほぼ1:1対応するような移植を行なった。とはいえもともと C++ なので TS であまりにも不自然なAPIは変更してる。

所感

完成度的にはまだ 70% ぐらい。SheetPin など全く実装してないものもある。色もない。とりあえず見るという目的にはある程度使えるかなというところ。

Chrome 拡張化して github 上に直接表示できるようにしたい。

元ファイルが行指向のフォーマットのため、diff をとって変更したコンポーネントやワイヤに色をつけるみたいなことも頑張ればできそう。

その他

いろいろ鑑みて webpack を初めて導入してみたが設定が辛かった。

2017年 08月 02日

node-canvas で scale したとき textBaseline が狂うのをなおした

2017年 07月 16日

関連画像を表示

libpuzzle の Perl binding である Image::Libpuzzle を使って関連画像を実装してみた。pHash や avgHash も試してみたけど、どれが良いかというとなんともいえない。

現像パラメータ違いみたいな写真同士のものはちゃんとスコアが高く出ている気がするが、それ以外の場合はいまいち似ているとは言えないような感じで、いまいちよくわからない。基本的に同一画像判定用な気がするので、あまり大きな期待はできなそう。

SQLite スキーマ

CREATE TABLE images (
	`id` INTEGER PRIMARY KEY,
	`uri` TEXT NOT NULL,
	`entry_id` INTEGER NOT NULL,
	`sig` BLOB NOT NULL
);
CREATE UNIQUE INDEX index_images_uri ON images (`uri`, `entry_id`);

CREATE TABLE ngram (
	image_id INTEGER NOT NULL,
	word BLOB NOT NULL,
	PRIMARY KEY (`image_id`, `word`)
) WITHOUT ROWID;
CREATE INDEX index_word_image_id ON ngram (`word`, `image_id`);

こういう感じのスキーマ。複数のエントリに同じURLを貼ると無駄になるけど滅多にないので気にしない。

実装

Service::SimilarImage にだいたいまとめて実装した。SQL 自体は php - Libpuzzle Indexing millions of pictures? - Stack Overflow に書いてあるのとほぼ同じ。