はじめに
ある日、履歴書用の証明写真が必要になりました。
コンビニのプリント機に行くか、写真館に行くか……と思いながら、ふと気づきます。
「え、自宅にプリンターあるじゃん」
「スマホで撮った写真あるじゃん」
「YOLOv8で顔と肩を検出して自動クロップすれば……自分で作れるじゃん?」
というわけで作りました。自宅で証明写真を生成してそのまま印刷できるPythonスクリプトです。
何ができるの?
- スマホやデジカメで撮った写真を入れると、YOLOv8のポーズ推定で頭と肩を自動検出してクロップ
- パスポート用・履歴書用・マイナンバー用など、サイズを選ぶだけ
- カスタムサイズも入力可能
- 生成後はそのままプリンターに送信して印刷(縁なし印刷対応)
- WSL2(Windows)とLinux(CUPS)の両方に対応
uv run main.py photo.jpg
たったこれだけです。あとは対話形式で聞かれる質問に答えていくだけ。
技術的なポイント
YOLOv8でポーズ推定
Ultralytics の yolov8n-pose.pt を使っています。17個のキーポイント(関節の座標)を検出できるモデルで、そのうち両肩(インデックス5, 6)とバウンディングボックスの上端を使って証明写真らしいクロップ範囲を計算しています。
頭の上に5%、肩の下に2%の余白を追加し、さらにZOOM係数1.1で少し引いて撮ったような自然な見た目に。なかなか絶妙な調整でしょ(自画自賛)。
印刷はPowerShell経由(WSL2の場合)
WSL2環境だとLinuxからWindowsのプリンターに直接アクセスできないので、subprocessでPowerShellを呼び出すというやや強引な方法をとっています。System.Drawing.Printing.PrintDocumentを駆使して縁なし印刷を実現。
IS_WSL = "microsoft" in Path("/proc/version").read_text().lower()
_PS_EXE, _PS_ENCODING = ("pwsh", "utf-8") if shutil.which("pwsh") else ("powershell.exe", "cp932")
苦労したこと(コミットログが語る歴史)
コミットログを見ると、いかに泥沼にはまったかがよく分かります。
UnicodeDecodeError との格闘(2コミット)
PowerShellの出力をデコードするとき、最初は普通に text=True でデコードしていました。
日本語Windowsで盛大に死にます。
日本語Windows環境では powershell.exe の出力がCP932(Shift-JIS)なのです。しかしPowerShell 7以上(pwsh)はUTF-8。なのでまず pwsh があるか確認して、なければ powershell.exe にフォールバックし、それぞれ別のエンコーディングを使うという苦肉の策に至りました。
_PS_EXE, _PS_ENCODING = ("pwsh", "utf-8") if shutil.which("pwsh") else ("powershell.exe", "cp932")
さらに -NoProfile を付けないとPowerShellがプロファイル読み込みでエラーを出すという罠もあり、別コミットで対処しています。「なんでプリントするだけでこんな苦労してるんだ……」と思いながら書いた記憶がある(はず)。
DecompressionBombError
スマホで撮った高解像度写真を読み込むと、Pillowが「爆発物を検出しました(比喩)」と言って止まります。Pillowにはデフォルトで1億画素の上限があり、最近のスマホ写真はそれを超えることがある。
Image.MAX_IMAGE_PIXELS = None # 高解像度カメラ画像の制限を解除
ワンライナーで解決しましたが、エラーメッセージに「DecompressionBombError」とあったときは一瞬ドキッとしました。
EOFError との戦い
対話形式でユーザーに番号入力をさせるとき、input() がEOFに達するとずっとループするという問題がありました。パイプやリダイレクトで実行したときなどに発生。
except EOFError:
print("\n入力がありません。終了します。")
sys.exit(1)
地味だけどこういう細かい処理、ちゃんとやっておかないとあとで「なんか無限ループしてる」ってなるやつです。
用紙サイズのマッチング問題
プリンターに縁なし印刷を指示するとき、L版 とか A4 とか名前で指定するのではなく、プリンタードライバーが持っている用紙サイズ一覧の中から近いものを探してセットしないとうまく動かないという仕様がありました。
サイズが完全一致ではなく「だいたい合ってる」レベルで判定するため、±10 hundredths(1/100インチ単位)の許容誤差を持たせています。
PAPER_SIZE_TOLERANCE_HUNDREDTHS = 10
「なんでこんなに面倒くさいんだ」と思いながら実装した部分その2。
「プリンターを先に選ぶ」問題
最初は「用紙サイズを先に聞いて、次にプリンターを選ぶ」という順番で作っていました。しかし用紙サイズってプリンターごとに違うじゃないですか。なので「プリンターを選んでから、そのプリンターがサポートする用紙サイズを取得して選ばせる」という順番に変更しました。
当たり前といえば当たり前ですが、最初は逆順で作っていたというのが人間らしいですよね(笑)。
実行の流れ
【プリンターを選んでください】
1. Canon_TR8600
2. HP_LaserJet
番号を入力してください: 1
【印刷用紙のサイズを選んでください】
1. L (89.0mm × 127.0mm)
2. KG (102.0mm × 152.0mm)
3. A4 (210.0mm × 297.0mm)
番号を入力してください: 1
【証明写真のサイズを選んでください】
1. 標準(縦30mm × 横24mm (3.0cm×2.4cm))
2. パスポート・マイナンバー(縦45mm × 横35mm (4.5cm×3.5cm))
3. 履歴書用・大(縦55mm × 横40mm (5.5cm×4.0cm))
4. カスタム入力
番号を入力してください: 2
... 画像生成 & プレビュー表示 ...
【給紙トレイを選んでください】
1. Auto
2. Cassette
番号を入力してください: 1
【印刷品質を選んでください】
1. 標準 (Medium) (600×600 dpi)
2. 高品質 (High) (1200×1200 dpi)
番号を入力してください: 2
【印刷してもよいですか?(用紙・電源を確認してください)】
1. はい、印刷する
2. いいえ、中止する
番号を入力してください: 1
印刷ジョブを送信しました。
最後に「印刷してもよいですか?」と確認するのは、「プリンターに用紙入れ忘れてた!」「プリンターの電源入れ忘れてた!」という事態を防ぐためです。プレビューを見てから、ゆっくり用紙をセットして、印刷する、という流れで使えます。
必要環境
- Python 3.11+
- uv(依存ライブラリの自動インストールもやってくれる)
- 印刷する場合: CUPS(Linux)または WSL2環境
初回実行時に yolov8n-pose.pt(約6MB)が自動ダウンロードされます。
リポジトリ: https://github.com/sakurai-keizi/id-photo-generator
まとめ
写真館に行かなくてよくなりました(たぶん)。品質的には写真館には及びませんが、履歴書に貼る程度なら全然問題ないレベルです。コンビニプリントより安いし、家から出なくていいのが最高。
最後に白状します
実は、このスクリプトのコードは全部 Claude Code が書きました。
自分でやったことといえば、「こういう機能が欲しい」と口で言うことと、動作確認と、あとはバグ報告くらいです。前述の「苦労した点」も、厳密には「Claude が苦労した点」です。私は横で見ていました。
そしてこの記事もClaudeに書いてもらいました。
もはや「AIに頼みすぎでは?」という声も聞こえてきそうですが、コードの意図や仕様を説明して動くものが出てくるなら、それはそれで一つのエンジニアリングのあり方なのかなと思っています。
「書けないけど、作れる」時代になってきた気がします。
……とはいえ、UnicodeDecodeErrorの原因を把握してエンコーディングを指定する判断も、DecompressionBombErrorへの対処も、全部Claudeがやっていたので、「把握している」かどうかも怪しいですが(笑)。
まあ、動いてるからいいか!