PCのカメラで写真を撮りExcelに取り込む

Python

私はどちらかというとブルーカラーな仕事の割合が多いので、作業の前後で写真をとって報告書に添付するということをよくやります。

このとき写真を貼り付けて印刷するフォーマットとして、当然ながら日本におけるスタンダードオブスタンダード、我らのExcelが使われます。

Excelで写真台帳作成

他社の報告書も見る機会がありますが、だいたいどこでもExcelで作成しているんだろうな~という見た目で、概ね次のようになっています。

写真はデジカメ等で撮影して、PCに取り込み、Excelシートへ貼り付けという手順を踏みます。

私はもちろん、貼り付けたいセルをクリックしたらVBAで 写真ファイル選択→リサイズ→貼り付け までの作業を自動化していますが、新米ペーペー作業員が作業前後で全く違うアングルから撮っていたり、写真看板に誤字脱字があったりして撮り直しというシチュエーションがままあり「これって撮影した瞬間にExcel貼り付けまでやってその場で確認できたら最強じゃね?」ということに気がついてしまいました。

デジカメやスマホのプレビュー画面で前後のアングルが合っているかを確認するのってかなり大変なんですよね。それが写真台帳の形式になっていれば一目瞭然です。

というわけで、写真撮影→Excel貼り付けまでを自動化します。例によってVBAにはそんなパワーはないので、肝心なところはPython先生にお出ましいただきます。

できるVBAerが持つべきPCはSurface

今回の要件を満たすにはPCに内蔵カメラが必要です。作業現場でノートPCを持ち歩くのはナンセンスで、しかもノートの内蔵インカメラはお察し性能なので、必然的にタブレット(2in1)PCになりますが、現状まともなWindowsタブレットはSurfaceしかないので一番安い「Go」を実証実験のために調達しました。

機種選定にあたりVBAをたしなむレベルのかたには言うまでもなさそうですが、ストレージがeMMCのやつはPCではなく「おもちゃ」なので買ってはいけません。それは仕事に使う道具ではありません。私は購入時点で最強スペックの SurfaceGo3 128G SSD にしました。

今回はじめてSurfaceGoをさわってみて、VBAerが1台は持っておくべきPCだと感じました。モバイル環境でフルスペックのOfficeアプリを使えることが、Excelの呪縛にとらわれ続ける我々VBAerの心に安寧をもたらします。これについては、別で語りたいと思います。

で、システム概要図は次のようになります。

写真撮影Pythonスクリプト

Pythonでカメラを使って写真を撮影するのは超簡単です。当サイトでもいくつものネタで登場しているOpenCV-Pythonがすべてやってくれます。

インストールしていないかたは今すぐしてください。

pip install opencv-python

コードもめちゃくちゃ簡単です。次のようにしました。

import cv2
import sys

WIN_NAME = 'Camera'


def mouse_click(event, x, y, f, p):
    if event == cv2.EVENT_LBUTTONDOWN:
        cv2.imwrite(file_path, frame)
        cap.release()


file_path = sys.argv[1]
img_width = int(sys.argv[2])
img_height = int(sys.argv[3])

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, img_width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, img_height)
cv2.namedWindow(WIN_NAME, cv2.WINDOW_AUTOSIZE)
cv2.setMouseCallback(WIN_NAME, mouse_click)

while cap.isOpened():
    ret, frame = cap.read()

    if ret:
        cv2.imshow(WIN_NAME, frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

    if not cv2.getWindowProperty(WIN_NAME, cv2.WND_PROP_VISIBLE):
        break

cap.release()

ざっと解説しますと、このスクリプトでは引数にファイルパス、画像幅、画像高さ(ピクセル)を取りカメラを起動して、表示されたカメラ映像ウィンドウをタップすると、そのときの映像を画像ファイルとして切り出し保存します。

mouse_click関数はcv2.setMouseCallbackメソッド(イベントリスナー)でウィンドウがクリック(タップ)されたときに実行されるコールバック関数です。引数は実際には第一しか使用しませんが、残りもcv2.setMouseCallbackメソッドから渡ってくるので、省略することはできません。何でもいいので仮引数名を与えておきます。

第一引数で渡ってきた値が左クリックを表すcv2.EVENT_LBUTTONDOWN定数だったらcv2.imwriteメソッドでそのときの映像を画像ファイルとして保存し、処理を終えます。

cv2.getWindowPropertyメソッドはOpenCVのウィンドウ表示状態を見ていて、ウィンドウを×ボタンで閉じた場合、通常はループで回しているのですぐにウィンドウが復活して表示されてしまいますが、このメソッドのはたらきで一度消されたらループから脱出するようになっています。これにより一般的なWindowsのウィンドウと同じ感覚で×ボタンで閉じることができるようになります。

あとは画像の縦横サイズを指定したり、ウィンドウの表示形式を設定したりしていますが、標準的なOpenCV-Pythonのカメラ映像処理コードとなっています。

写真ファイル貼り付けVBA

Excel側からは先のPythonコードへ各引数を渡して、Pythonが撮影した画像ファイルをシートへ貼り付けます。

Pythonへ渡す引数、その他パラメーターは変更しやすいようにシートから入力するようにしておきます。

VBAコードは次のようにしました。

Sub takePicture(Target As Range)
    On Error GoTo errhandle
    
    Dim ws As Worksheet
    Set ws = Target.Parent
    
    Dim scriptPath As String
    Dim picPath As String
    Dim isSave As Boolean
    Dim picW As String
    Dim picH As String
    scriptPath = ws.Range("e4").Value
    picPath = ws.Range("e5").Value & "\" & Format(Now, "yyyymmdd-hhmmss") & ".jpg"
    isSave = ws.Range("e6").Value = "する"
    picW = ws.Range("e3").Value
    picH = ws.Range("f3").Value
    
    Target.Value = "カメラを起動中"
    
    Dim wss As Object
    Set wss = CreateObject("WScript.Shell")
    wss.Run "python " & scriptPath & " " & picPath & " " & picW & " " & picH, 0, True
    
    Dim pic As Shape
    Set pic = ws.Shapes.AddPicture(picPath, False, True, Left:=Target.Left, Top:=Target.Top, Width:=-1, Height:=-1)
    pic.LockAspectRatio = msoTrue
    pic.Width = Target.Width
    
    Target.Offset(1).Select
    
    If Not isSave Then
        Kill picPath
    End If
    
    Target.Value = "余 白"
    ws.Cells(Target.Row + 5, Target.Column - 1).Value = Now
    
    Exit Sub
    
errhandle:
    Target.Value = "余 白"
End Sub

このプロシージャをシートのWorksheet_BeforeRightClickイベントからコールします。どこでも右クリックから起動してしまうと困るので、写真貼り付け枠だけから起動するよう条件をつけておきます。

Private Sub Worksheet_BeforeRightClick(ByVal Target As Range, Cancel As Boolean)
    If Target.Column = 2 Then
        If Target(1, 1).Value = "余 白" Then
            Call takePicture(Target)
            Cancel = True
        End If
    End If
End Sub

特に難しいことはしていないですが簡単に解説しますと、まず最初にシートからパラメーターを取得します。

scriptPathは先のPythonスクリプトが置いてある場所を指定します。

picPathは画像ファイルのフルパスで、シートで指定したフォルダにタイムスタンプをファイル名にしてパスを作成しています。ここへPythonから画像ファイルが書き出されます。

picWpicHはそのままで、画像のサイズです。ここはカメラによって指定できる値が決まっているらしく、私の場合アス比4:3では640×480と1920×1440はうまくいきました。それ以外の一般的なピクセル幅を指定しても勝手にアス比が変わってしまいうまくいきませんでした。

isSaveは書き出した画像ファイルをそのまま保存しておくか、シートに貼り付けたらすぐ消すかのフラグです。シート側で「する・しない」を入力規則リストから選択します。

写真を貼り付けたいセルを右クリック(ロングタップ)するとシートから取得したパラメーターをWScript.Shellでコマンドライン起動したPythonスクリプトに引数として渡します。Pythonスクリプト側でカメラを制御してカメラ映像がウィンドウに表示されます。

WScript.Shellは同期実行されているため、Pythonスクリプトが終了するまでVBA側の処理は止まっています。ウィンドウをクリック(タップ)すると映像が画像ファイルとして書き出されウィンドウが閉じます。

ここでVBAへ処理が戻ります。保存先のパスはVBA側で作成したものなので、それを読み込み右クリックされたセルのサイズに合わせて貼り付けます。

ws.Cells(Target.Row + 5, Target.Column – 1).Value = Nowで撮影日時を所定のセルへ自動入力しています。

写真を撮影しないでウィンドウを閉じると、ファイルが作成されずに実行時エラーになるのでエラーハンドラーで回避します。

Python→Excel間をファイルではなくストリームで受け渡せたらスマートで超カッコイイのですが、できるとしても超絶面倒なコードになりそうな予感しかしないので考えないでおきます。

デモ動画

実際のうごきを動画にしました。

概ね期待通りになりました。ちょっとコードを書き換えれば、ウィンドウを閉じずにタップするごとに貼り付けるセルを変えて連続して写真を貼っていくこともできそうですね。

なお、タブレットPCモードではスタイラスペンを使う場合と指で操作する場合でExcelの挙動が変わります。ペンならロングタップで右クリックイベントが発火するのですが、指だとダメでした。

VBA側でタッチを判定するイベントを実装してくれればやりやすいのですが、そんなことはもうないでしょう。というのも、Excelにも「描画」というタブがいつのまにかできていて、スタイラスペンでフリーハンドでシートへ書き込みができるなど今風に進化していますが、VBAからは描画タブにあるツールを全く触ることができません。マクロの自動記録で記録しようとしても何も記録してくれません。Microsoftがこれ以上VBAを進化させるつもりがないことがハッキリとわかってしまい、VBAおじさんは悲しいです。

まとめ

Surfaceを使ってPythonと連携することでVBAもマルチメディア分野で輝ける可能性が見えてきました。過去のネタで扱ったバーコードリーダーもSurfaceのカメラなら問題なく使えますのでいろいろできそうです。

最後に巷で散々言われておりますが、MicrosoftさまSurfaceにOfficeアプリのライセンスを抱き合わせ販売するのはやめてください。私は個人なのに365サブスク料を上納しているハイクラスMicrosoft奴隷でございます。

おわり。