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)
}
  1. トップ
  2. tech
  3. Mac でウェブカメラの定期撮影を Swift で書く

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

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

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

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

  1. トップ
  2. tech
  3. デスク整理用のなにか

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

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

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

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

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

  1. トップ
  2. tech
  3. Prusa i3 MK2S のベッドにウェブカメラC270をつける

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

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 で出力。

  1. トップ
  2. tech
  3. Prusa i3 MK2S のエクストルーダー付近