airpocketのアイコン画像
airpocket 2024年09月24日作成 (2024年10月16日更新) © MIT
製作品 製作品 閲覧数 275
airpocket 2024年09月24日作成 (2024年10月16日更新) © MIT 製作品 製作品 閲覧数 275

ぜんまい仕掛けのデジタルカメラ Hi-Vision

ぜんまい仕掛けのデジタルカメラ Hi-Vision

はじめに

このプロジェクトは、ゼンマイ仕掛けのデジタルムービーカメラ(Ver.1 完成)の改良したVer.2を改題したものです。

このプロジェクトではDouble 8規格のゼンマイ駆動式8mmフィルムシネカメラをデジタルカメラに改造します。
目的は、ゼンマイ駆動式の8mmフィルムカメラによる撮影を手軽に体験し、その価値の再認識をうながし失われつつある8mmフィルムカメラを保護するきっかけにすることです。
改造を行うことで、8mmフィルムカメラとしては機能しなくなるため、使用可能なカメラを改造に供することは可能な限りお控え下さい。

記事作成時現在、市場には付属レンズを失ったカメラボディが多数販売されています。シネカメラ用Dマウントレンズは多くが他市場に流れて、シネカメラとして使用できないボディが入手できます。これらのカメラの多くはレンズ交換式のため、レンズを失ったボディをデジタルカメラに改造し、フィルム撮影可能なカメラからレンズを「借りて」使用する等を想定しています。

完成品紹介

このカメラでこのような動画が撮影できました。

ここに動画が表示されます

Yashica 8

このカメラは、1958年に発売されたYashica 8というカメラをデジタルカメラに改造したものです。
Yashica 8 はDouble8という規格のフィルムを用いたシネカメラです。設計はスイス製のBolex C-8を参考に進められていた様で、駆動部などの基本設計はほぼそのまま流用されています。駆動力はぜんまい式、撮影用とファインダー用の2系統の光学系を持ち、撮影用レンズはターレット式の交換機構が組み込まれています。

改造の方針

デジタル化改造を行うにあたり、カメラ本体のゼンマイ駆動部は残置しつつフィルム送り部を取り除いてデジタル撮影モジュールを設置するスペースを確保しました。
Ver.1ではカメラ本体への改造は難しいと判断して、フィルムマガジン式カメラの改造を行いましたが、当時のカメラ複数台を分解、構造を確認してカメラ本体への改造も可能となりました。
これにより当時のゼンマイ式カメラの多くが改造可能となり、順次改造を進めています。

また、Ver.1ではシャッタ駆動のとシンクロを行っていましたが、今プロジェクトではシャッター膜を取り外してシャッタ駆動とのシンクロはオミットしました。これには種々の理由があるのですが、この改造方針の変更により撮影機能が向上し、以下の様なカメラ機能のスペックアップを実現しました。

操作性

最も重要な部分である、「オリジナルカメラと同じ操作性」を確保しています。
・シャッター動作はぜんまい駆動
・光学系はオリジナル。フォーカス、露出はマニュアル設定

デジタル撮影ユニットをセットした後は、当時の全く同じ手順での撮影が可能なため、フィルムカメラ撮影の難しさや手間、そして撮影の手ごたえや快感はそのままにデジタルカメラの気楽さで撮影が可能となりました。
操作方法は以下の動画で確認できます。

ここに動画が表示されます

デジタル化のメリット

このカメラでは8mmフィルムカメラの特長を残しつつ以下の様なデジタルカメラのメリットを得られます。

・WiFi接続、もしくはUSB接続を用いたデータ出力が可能
・ストリーミングカメラ機能で、Webカメラとしても使用可能

ネットワークに接続し、ファイルサーバとして動画ファイルの出力が可能なため、手軽に動画確認ができます。ストリーミング出力機能も実装したため、以下の様にWebカメラがわりに使用することもできます。

@youtube

主な仕様

項目 Ver.1 Hi-Vision(Ver.2) オリジナル
コントローラ Zero 2W Zero 2W -
イメージセンサ IMX219 IMX708 -
解像度 640x480 1920x1080 -
視野(Double8 比 31% 110% 4.5x3.3mm
連続撮影時間 6sec 45sec以上 45sec
フレームレート 最大32 最大64 最大64

外観

キャプションを入力できます
http://yashicatlr.com/Yashica8.html

撮影ユニット

撮影ユニット開発は、カメラのサイズに合わせて筐体を設計し、制御基板、電源、コントローラ用Raspberry Zero2Wなどを配置しました。
筐体内レイアウトは試行錯誤しながらパズルを解く感覚で検討し、ユニバーサル基板を用いて初期の開発基板を作りんでいます。
初期開発基板

初期開発基板で動作確認を行ったのち、PCBにて本番基板を制作、ユニット化しました。
PCBによるモジュール外観
PCBによるモジュール内部

部品

部品表

3D部品以外の部品表

No 品名 単価 数量 小計 購入先
1 フロアプレート固定ネジM2x4 5 AliExpress
2 ケーストップ固定ネジM2.5x4 6 AliExpress
3 基板固定ネジ M2x4 4 AliExpress
4 RasPi固定ネジ M2x4 3 AliExpress
5 RasPiカメラ基板固定ネジ M2x4 4 AliExpress
6 リフレクタ固定用ねじM1.6x6 1 AliExpress
7 006P バッテリー 2358 1 AMAZON
8 オリジナル基板 1 JLCPCB
9 9V電池端子 1 AliExpress
10 dipswitch 6way 1 AliExpress
11 tactswitch 6x6x8mm 1 AliExpress
12 超小型スライドスイッチ 2回路2接点 IS-2235 1 AliExpress
13 φ3 砲弾LED RED 1 AliExpress
14 φ3 砲弾LED GREEN 1 AliExpress
15 ステップダウンモジュール MP2307 1 AliExpress
16 ロープロファイルピンヘッダー (低オス) 2×40 (80P) 7.7mm 40 1 秋月電子
17 ダブルピンソケット (低メス) 2×13 (26P) 80 2 秋月電子
18 LM397 SOT23-5 1 AliExpress
19 SMD 0805抵抗 R1 36K 1 AliExpress
20 SMD 0805抵抗 R2 1K 1 AliExpress
21 SMD 0805抵抗 R3 12K 1 AliExpress
22 SMD 0805抵抗 R4 220 1 AliExpress
23 SMD 0805抵抗 R5 2K 1 AliExpress
24 SMD 0805抵抗 R6 2K2 1 AliExpress
25 SMD 0805抵抗 R7 220 1 AliExpress
26 SMD 0805抵抗 R8 2K4 1 AliExpress
27 SMD 0805抵抗 R9 1K 1 AliExpress
29 Raspberry Pi Zero2 W 2915 1 SWITCH SCIENCE
30 arducam IMX708 fix focus 4147 1 SWITCH SCIENCE
31 arducam camera extension cable 200mm 1 UCTRONICS
32 0.5mm pitch 22pin FPC cable Reverse 100mm 1 AliExpress

※表面実装抵抗はinchスケール

3Dデータ

カメラ本体の改造及びデジタルカメラモジュールの筐体に必要な3Dプリント部品はGitHubで公開しています。

回路および基板

回路

回路図は以下の通りです。
キャプションを入力できます

PCB

開発初期はユニバーサル基板を用いていましたが、今はPCBを起こしています。
GitHubにJLCPCB用の発注データを含むKiCAD8のデータを公開しています

キャプションを入力できます

コード

カメラ撮影用

# GPIOをトリガーにビデオモードで撮影する。 # シャッター除去モデル使用、シャッターシンクロはしない。 # IMX708をビデオモードで撮影 # IMX708 ビデオモード仕様 1080P 50FPS, 720p 100FPS, 480p 120FPS import cv2 import os import time import RPi.GPIO as GPIO import sys from picamera2 import Picamera2 from picamera2.encoders import H264Encoder from picamera2.outputs import FfmpegOutput import libcamera import numpy as np import re import shutil number_cut = 0 # 動画ファイルの連番を格納 number_still = 0 # 静止画ファイルの連番を格納 # Bolex/yashica pin_shutter = 25 # shutter timing pickup # GPIO設定 GPIO.setmode(GPIO.BCM) GPIO.setup(pin_shutter, GPIO.IN, pull_up_down = GPIO.PUD_UP) is_recording = False #録画中フラグ max_record_sec = 45 # 最大撮影時間 record_fps = 16 # MP4へ変換する際のFPS設定値 temp_folder_path = "/tmp/" share_folder_path = "/home/cinecamera/share/" device_name = "yashica" codec = cv2.VideoWriter_fourcc(*'avc1') last_shutter_time = 0 shutter_release_threshold_time = 400 #msec シャッター開放判定の閾値 recording_start_time = 0 # shareフォルダの動画と静止画のファイル番号を読み取る def find_max_number_in_share_folder(): global number_still, number_cut # ファイル名の数字部分を抽出する正規表現パターン pattern_mp4 = re.compile(r'{}(\d{{3}})\.mp4$'.format(device_name)) pattern_jpg = re.compile(r'{}(\d{{3}})\.jpg$'.format(device_name)) number_cut = -1 # 存在する数字の中で最も大きいものを保持する変数 number_still = -1 # 指定されたフォルダ内のすべてのファイルに対してループ for filename in os.listdir(share_folder_path): match_mp4 = pattern_mp4.match(filename) match_jpg = pattern_jpg.match(filename) if match_mp4: # ファイル名から抽出した数字を整数として取得 number = int(match_mp4.group(1)) # 現在の最大値と比較して、必要に応じて更新 if number > number_cut: number_cut = number if match_jpg: number = int(match_jpg.group(1)) if number > number_still: number_still = number number_still += 1 number_cut += 1 def init_camera(): global camera, encoder camera = Picamera2() encoder = H264Encoder(bitrate=10000000) """ video_config = camera.create_video_configuration( main={"size": (1920, 1080), "format": "YUV420"}, controls={ #"ExposureTime": 20000, #"AnalogueGain": 1.0, #"AwbMode": "auto", "Brightness": 50, "Contrast": 15, "Saturation": 20, "Sharpness": 10, #"NoiseReductionMode": "high", "FrameRate": 16 } ) """ video_config = camera.create_video_configuration( main={"size": (1920, 1080)}, #controls={ # "ExposureTime" :50000, # "AnalogueGain" :1.0, # "AwbMode" :False, # "Brightness" :0.0, # "Contrast" :1.0, # "Saturation" :1.0, # "Sharpness" :1.0, # "NoiseReductionMode":1, # #"ColourGains" :(0.0, 0.0, 0), # "FrameRate" :16 #}, transform = libcamera.Transform(hflip=1, vflip=1), buffer_count = 8 ) camera.configure(video_config) camera.start() def shutter_detected(channel): global last_shutter_time, recording_start_time, is_recording current_time = int(time.perf_counter() * 1000) #print("detect") if not is_recording: print("start recording") movie_file_path = temp_folder_path + device_name + "{:03}".format(number_cut) + ".mp4" output = FfmpegOutput(movie_file_path) camera.start_recording(encoder, output) is_recording = True recording_start_time = current_time last_shutter_time = current_time def check_recording_status(): global is_recording, number_cut if is_recording: current_time = int(time.perf_counter() * 1000) print(str(current_time-last_shutter_time)) if current_time - last_shutter_time > shutter_release_threshold_time or current_time - recording_start_time > max_record_sec * 1000: camera.stop_recording() #camera.stop() print("stop recording") print("start:" + str(recording_start_time) + " diff: " + str(current_time-last_shutter_time) + " video: " + str(current_time - recording_start_time)) temp_file_path = temp_folder_path + device_name + "{:03}".format(number_cut) + ".mp4" save_file_path = share_folder_path + device_name + "{:03}".format(number_cut) + ".mp4" #cut_first_8_frames(temp_file_path, save_file_path) #os.remove(temp_file_path) shutil.move(temp_file_path, save_file_path) is_recording = False number_cut += 1 #camera.start() def cut_first_8_frames(input_path, output_path): # ビデオキャプチャを開始 cap = cv2.VideoCapture(input_path) # 出力ビデオの設定 fourcc = cv2.VideoWriter_fourcc(*'mp4v') fps = cap.get(cv2.CAP_PROP_FPS) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) # 最初の8フレームをスキップ for _ in range(8): cap.read() # 残りのフレームを読み込み、出力ビデオに書き込む while True: ret, frame = cap.read() if not ret: break out.write(frame) # リソースの解放 cap.release() out.release() print("finish cut") if __name__ == "__main__": # shareフォルダ内の動画、静止画ファイルの最大番号を取得 find_max_number_in_share_folder() init_camera() GPIO.setmode(GPIO.BCM) GPIO.setup(25, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect(pin_shutter, GPIO.FALLING, callback=shutter_detected, bouncetime=200) try: while True: check_recording_status() time.sleep(0.1) finally: camera.stop() GPIO.cleanup()

Flaskによるストリーミング用コード

from flask import render_template, Flask, Response import cv2 from picamera2 import Picamera2 import time import numpy as np from typing import List app = Flask(__name__, template_folder='templates') #templates_folderはデフォルトで'templates'なので本来定義は不要 CAP_WIDTH = 640 #出力動画の幅 CAP_HEIGHT = 480 #出力動画の高さ LAW_WIDTH = 1640 #カメラ内のraw画像の幅 LAW_HEIGHT = 1232 #カメラ内のraw画像の高さ image_sensor = "IMX708" #IMX219/IMX708 folder_path ="/tmp/img" movie_length = 100 #撮影するフレーム数 time_list = [] exposure_time = 5000 #イメージセンサの露出時間 analog_gain = 20.0 #イメージセンサのgain def gen_frames(): print("gen_frames") count = 0 # init camera cap = Picamera2() config = cap.create_still_configuration(main={"size":(CAP_WIDTH, CAP_HEIGHT)},raw={"size":(LAW_WIDTH,LAW_HEIGHT)}) cap.configure(config) cap.set_controls({"ExposureTime":exposure_time, "AnalogueGain": analog_gain}) cap.start() while True: print("count = ",count) start_time_frame = time.perf_counter() frame = cap.capture_array() frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frame = cv2.flip(frame,-1) #フレームデータをjpgに圧縮 ret, buffer = cv2.imencode('.jpg',frame) # bytesデータ化 frame = buffer.tobytes() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') elapsed_time_frame = time.perf_counter() - start_time_frame print("frame_number = " + str(count) + " / time = " + str(elapsed_time_frame)) count +=1 @app.route('/video_feed') def video_feed(): #imgタグに埋め込まれるResponseオブジェクトを返す return Response(gen_frames(), mimetype='multipart/x-mixed-replace; boundary=frame') @app.route('/') @app.route('/index') def index(): user = {'username' : 'Raspberry Pi zero2 W', 'image sensor' : image_sensor, 'lens' : ""} return render_template('index.html', title='home', user=user)

flask用index.html

<html> <head> <title>{{ title }} CAMERA STREAMING TEST</title> </head> <body> <h3>from {{ user.username }}.</h3> <h3>CAMERA STREAMING TEST.</h3> <img src="{{ url_for('video_feed') }}"> <h3>lens:{{ user.lens }}</h3> </body> </html>

Raspberry Pi Zero 2Wのセットアップ

項目 要件
SBC Raspberry Pi Zero2 W
microSD 32GB以上
OS Bullseye 64bit lite

OS version

OSはRaspberry Pi OS Bullseye 64bit liteのみ対応しています。
現在GPIOライブラリの関係で、Bookwormには対応していません。

$ lsb_release -a No LSB modules are available. Distributor ID: Debian Description: Debian GNU/Linux 11 (bullseye) Release: 11 Codename: bullseye $ getconf LONG_BIT 64

USB SSHの有効化

起動用SDカードを書き込み後、config.txtに以下の行を追記

dtoverlay=dwc2

commandline.txtのrootwait とquietの間に[]の中を追記

ootwait [modules-load=dwc2,g_ether] quiet

USB OTGするときは左側のmicro USBコネクタ
windowsにもUSB Ethernet/RNDIS Gadgetのドライバインストールが必要です

wifi setting

sudo raspi-config

1 System Options -> S1 Wireless LAN
SSIDとPassphraseを入力してリブート

必要なライブラリ等のインストール

sudo apt update && sudo apt -y upgrade sudo apt -y install python3-dev python3-pip # sudo pip install picamera2 sudo pip install opencv-python sudo apt -y install libgl1-mesa-dev

IMX708を使用するための設定

sudo nano /boot/config.txt

最後に以下の一行を追記

dtoverlay=imx708

swap無効化とtempのRAMディスク化

sudo systemctl stop dphys-swapfile sudo systemctl disable dphys-swapfile

ファイルシステムの設定を書き換えて/tmpをRAM上にマウントする

sudo nano /etc/fstab

以下の行を追加

tmpfs /tmp tmpfs defaults,size=64m,noatime,mode=1777 0 0

microSD上の/tmpを削除する

sudo rm -rf /tmp

ここで一度リブート

RAMの状況を確認するとこんな感じ

$ free -m total used free shared buff/cache available Mem: 419 73 193 0 151 292 Swap: 0 0 0 $ df -h Filesystem Size Used Avail Use% Mounted on /dev/root 29G 1.9G 26G 7% / devtmpfs 80M 0 80M 0% /dev tmpfs 210M 0 210M 0% /dev/shm tmpfs 84M 928K 83M 2% /run tmpfs 5.0M 4.0K 5.0M 1% /run/lock tmpfs 128M 0 128M 0% /tmp /dev/mmcblk0p1 255M 31M 225M 13% /boot tmpfs 42M 0 42M 0% /run/user/1000

Flaskのインスト―ル

ストリーミングにはFlask使っています

sudo pip install flask

flaskでapp.pyを実行するには

flask run --host=0.0.0.0

sambaサーバー設定

インストールと共有フォルダの作成など
[user]にはraspberry pi zer2w ログイン時のuser名を入れる

sudo apt -y install samba mkdir /home/[user]/share sudo chmod 777 /home/[user]/share sudo nano /etc/samba/smb.conf

smb.conf の最後に以下を追記

[share] comment = user file space path = /home/[user]/share force user = [user] guest ok = yes create mask = 0777 directory mask = 0777 read only = no
sudo systemctl restart smbd

開発環境について

ソフト開発等する場合は、Sambaで共有したフォルダにGitHubのリポジトリをクローンして開発用PC上のVSCodeからSSH接続するのが楽。
vimer は直接開発してください。

開発用PCからVSCodeでSSH接続して、接続先となったzero2のフォルダを開こうとすると叱られが発生するので以下の様に回避。

共有フォルダのGitを利用する際。。。
git: UNC host '****' access is not allowedの回避方法
GitがUNCホスト表記を禁止しているため。
「設定」から、Allowed UNC Hostsを検索し、「項目の追加」から使用したいホスト名を登録、VSCodeを再起動すると使用できるようになる。

自動起動

zero2起動時に自動的にカメラ制御ソフトを実行するにはcrontabが良い

crontab -e

以下の一行を追記

@reboot python [/path/of/file.py]

[/path/of/file.py]にはカメラ制御コードをの絶対パスを記入
自動起動したpythonコードを停止するには

ps aux | grep pytnon

でプロセスナンバー確認して

kill [number]

ffmpegのインストール

mpegのメタデータ埋め込みにffmpegを使用しています
python用のラッパーも入れておく

sudo apt -y install ffmpeg sudo pip install ffmpeg-python

vimもあった方が玄人っぽいので。

sudo apt install vim

vimでも行番号表示、シンタックスハイライト、タブを4文字スペースに変更すると書ける

vim ~/.vimrc
set number syntax enable set expandtab set tabstop=4 set shiftwidth=4
1
airpocketのアイコン画像
電子工作、プログラミング、AI、DIY、XR、IoT M5Stack / Raspberry Pi / Arduino / spresense / K210 / ESP32 / Maix / maicro:bit / oculus / Jetson Nano / minipupper etc
ログインしてコメントを投稿する