はじめに
このプロジェクトは、ゼンマイ仕掛けのデジタルムービーカメラ(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カメラがわりに使用することもできます。
主な仕様
項目 | 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にて本番基板を制作、ユニット化しました。
部品
部品表
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
投稿者の人気記事
-
airpocket
さんが
2024/09/24
に
編集
をしました。
(メッセージ: 初版)
-
airpocket
さんが
2024/09/25
に
編集
をしました。
-
airpocket
さんが
2024/10/07
に
編集
をしました。
-
airpocket
さんが
2024/10/07
に
編集
をしました。
-
airpocket
さんが
2024/10/07
に
編集
をしました。
-
airpocket
さんが
2024/10/16
に
編集
をしました。
ログインしてコメントを投稿する