はじめに
リモートワークの普及に伴い、作業状況を可視化するツールへの需要が高まっています。GPT-4 VisionなどのLMM(Large Multimodal Model)を使えば高精度な解析が可能ですが、APIコストが高いのが悩みどころです。
本記事では、Pythonの従来型画像処理技術(OCR + OpenCV)を使って、低コストで作業ログシステムを構築する方法を解説します。
対象読者
- リモートワーク管理ツールに興味がある方
- LMM APIのコストに悩んでいる方
- Python + OpenCVでの画像処理に興味がある方
システム概要
実現する機能
- 画面解析:スクリーンショットからOCRでテキスト抽出し、作業内容を推定
- 在席判定:Webカメラから顔検出で在席/離席を判定
- 作業ログ記録:タイムスタンプ付きでデータベースに保存
技術スタック
- Python 3.9+
- Tesseract OCR / EasyOCR:画面テキスト抽出
- OpenCV:Webカメラ映像処理・顔検出
- Pillow:スクリーンショット取得
- SQLite:ログデータ保存
セットアップ
必要なライブラリのインストール
# 基本ライブラリ
pip install opencv-python pillow easyocr
# Tesseract OCR(高速・軽量)
# Windows: https://github.com/UB-Mannheim/tesseract/wiki
# Mac: brew install tesseract
# Linux: sudo apt-get install tesseract-ocr
pip install pytesseract
# データベース(標準ライブラリ)
# sqlite3は標準で含まれています
実装:画面解析(OCR)
スクリーンショット取得 + OCR
import pytesseract
from PIL import ImageGrab
import re
from datetime import datetime
class ScreenAnalyzer:
def __init__(self, lang='jpn+eng'):
"""
言語設定:日本語+英語のOCR
"""
self.lang = lang
def capture_screen(self):
"""
画面全体をキャプチャ
"""
screenshot = ImageGrab.grab()
return screenshot
def extract_text(self, image):
"""
OCRでテキスト抽出
"""
text = pytesseract.image_to_string(image, lang=self.lang)
return text.strip()
def classify_activity(self, text):
"""
抽出テキストから作業内容を推定
"""
text_lower = text.lower()
# キーワードベースの分類
if any(keyword in text_lower for keyword in ['vscode', 'pycharm', 'def ', 'class ', 'import']):
return 'コーディング'
elif any(keyword in text_lower for keyword in ['chrome', 'firefox', 'http', 'www']):
return 'ブラウジング'
elif any(keyword in text_lower for keyword in ['excel', 'word', 'powerpoint', '.xlsx', '.docx']):
return 'ドキュメント作成'
elif any(keyword in text_lower for keyword in ['slack', 'teams', 'zoom', 'meet']):
return 'コミュニケーション'
else:
return '不明'
def analyze(self):
"""
画面解析の実行
"""
screenshot = self.capture_screen()
text = self.extract_text(screenshot)
activity = self.classify_activity(text)
return {
'timestamp': datetime.now().isoformat(),
'activity': activity,
'text_sample': text[:100] # 最初の100文字のみ保存
}
# 使用例
analyzer = ScreenAnalyzer()
result = analyzer.analyze()
print(f"作業内容: {result['activity']}")
print(f"抽出テキスト: {result['text_sample']}")
EasyOCRを使った高精度版
Tesseractで精度が出ない場合は、EasyOCRを試してみましょう。
import easyocr
class ScreenAnalyzerEasy:
def __init__(self):
# 初回実行時にモデルダウンロード(時間がかかる)
self.reader = easyocr.Reader(['ja', 'en'], gpu=False)
def extract_text(self, image):
"""
EasyOCRでテキスト抽出
"""
results = self.reader.readtext(image)
# 検出されたテキストを結合
text = ' '.join([result[1] for result in results])
return text.strip()
比較:Tesseract vs EasyOCR
| 項目 | Tesseract | EasyOCR |
|---|---|---|
| 速度 | 高速 | 低速 |
| 精度 | 中 | 高 |
| GPU対応 | なし | あり |
| 初期設定 | 要インストール | pip install のみ |
実装:人物検出(OpenCV)
Webカメラから顔検出
import cv2
from datetime import datetime
class PersonDetector:
def __init__(self):
"""
Haar Cascade分類器の初期化
"""
self.face_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
)
self.cap = cv2.VideoCapture(0) # 0はデフォルトカメラ
def detect_face(self):
"""
顔検出による在席判定
"""
ret, frame = self.cap.read()
if not ret:
return {'present': False, 'error': 'カメラ読み取り失敗'}
# グレースケール変換(顔検出の前処理)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 顔検出
faces = self.face_cascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30)
)
return {
'timestamp': datetime.now().isoformat(),
'present': len(faces) > 0,
'face_count': len(faces)
}
def release(self):
"""
カメラリソースの解放
"""
self.cap.release()
# 使用例
detector = PersonDetector()
result = detector.detect_face()
print(f"在席状況: {'在席' if result['present'] else '離席'}")
detector.release()
より高精度な顔検出(dlib)
Haar Cascadeで精度が出ない場合は、dlibを試してみましょう。
pip install dlib
import dlib
class PersonDetectorDlib:
def __init__(self):
self.detector = dlib.get_frontal_face_detector()
self.cap = cv2.VideoCapture(0)
def detect_face(self):
ret, frame = self.cap.read()
if not ret:
return {'present': False, 'error': 'カメラ読み取り失敗'}
# RGB変換(dlibはRGB形式を期待)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 顔検出
faces = self.detector(rgb, 1)
return {
'timestamp': datetime.now().isoformat(),
'present': len(faces) > 0,
'face_count': len(faces)
}
実装:統合システム
定期実行 + データベース保存
import sqlite3
import time
import threading
class WorkLogSystem:
def __init__(self, db_path='work_log.db'):
self.db_path = db_path
self.screen_analyzer = ScreenAnalyzer()
self.person_detector = PersonDetector()
self.init_database()
def init_database(self):
"""
SQLiteデータベースの初期化
"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS work_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
activity TEXT,
present INTEGER,
text_sample TEXT
)
''')
conn.commit()
conn.close()
def log_work(self):
"""
作業ログの記録
"""
# 画面解析
screen_result = self.screen_analyzer.analyze()
# 在席判定
person_result = self.person_detector.detect_face()
# データベース保存
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO work_logs (timestamp, activity, present, text_sample)
VALUES (?, ?, ?, ?)
''', (
screen_result['timestamp'],
screen_result['activity'],
1 if person_result['present'] else 0,
screen_result['text_sample']
))
conn.commit()
conn.close()
print(f"[{screen_result['timestamp']}] "
f"作業: {screen_result['activity']}, "
f"在席: {'○' if person_result['present'] else '×'}")
def start_monitoring(self, interval=300):
"""
定期監視の開始(デフォルト5分間隔)
Args:
interval: 記録間隔(秒)
"""
print(f"監視開始({interval}秒間隔)")
try:
while True:
self.log_work()
time.sleep(interval)
except KeyboardInterrupt:
print("\n監視終了")
self.person_detector.release()
def get_summary(self, date=None):
"""
作業サマリーの取得
"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
if date:
cursor.execute('''
SELECT activity, COUNT(*) as count
FROM work_logs
WHERE date(timestamp) = ?
GROUP BY activity
''', (date,))
else:
cursor.execute('''
SELECT activity, COUNT(*) as count
FROM work_logs
GROUP BY activity
''')
results = cursor.fetchall()
conn.close()
return results
# 使用例
if __name__ == '__main__':
system = WorkLogSystem()
# 5分間隔で監視開始
system.start_monitoring(interval=300)
# サマリー表示(別途実行)
# summary = system.get_summary()
# for activity, count in summary:
# print(f"{activity}: {count}回")
コスト比較:LMM vs OCR+OpenCV
料金試算(1日8時間、5分間隔で記録)
| 方式 | 1回あたり | 1日(96回) | 月額(20営業日) |
|---|---|---|---|
| GPT-4 Vision | $0.01 | $0.96 | $19.2 |
| OCR + OpenCV | $0 | $0 | $0(電気代・サーバー代のみ) |
圧倒的なコスト優位性!
精度比較
| 項目 | LMM | OCR + OpenCV |
|---|---|---|
| 作業内容の詳細理解 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| テキスト抽出精度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 在席判定 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 表情・集中度分析 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
ハイブリッドアプローチの提案
最もコスト効率が良い方法:
class HybridAnalyzer:
def __init__(self):
self.ocr_analyzer = ScreenAnalyzer()
self.confidence_threshold = 0.7
self.llm_call_count = 0
self.max_llm_calls_per_day = 10 # コスト制限
def analyze_with_fallback(self):
"""
通常はOCR、不明瞭な場合のみLMM
"""
ocr_result = self.ocr_analyzer.analyze()
# 「不明」判定が多い場合、LMMで再解析
if (ocr_result['activity'] == '不明' and
self.llm_call_count < self.max_llm_calls_per_day):
print("OCR判定不明 → LMMで再解析")
# ここでLMM APIを呼び出す
# llm_result = call_gpt4_vision(screenshot)
self.llm_call_count += 1
return llm_result
return ocr_result
戦略:
- 通常時はOCR + OpenCVで処理(コスト $0)
- 判定が曖昧な時だけLMMに問い合わせ(月10回まで = $0.1)
- 総コスト:月額 $0.1 vs 純粋LMMの $19.2(99%削減!)
制約と課題
OCRの制約
- 画像・グラフ・UIアイコンからの意味理解は困難
- 文脈理解が必要な複雑な判定は精度が低い
- 日本語OCRは環境によって精度にばらつきがある
OpenCVの制約
- 表情や集中度の詳細分析は困難
- 照明条件に敏感(暗い環境では検出率低下)
- Haar Cascadeは横顔や角度のある顔の検出が苦手
改善案
- MediaPipeで姿勢推定を追加(前のめり度で集中度を推定)
- アクティブウィンドウ取得でより正確な作業分類
- キーボード・マウスの操作ログと組み合わせて精度向上
プライバシー配慮
実装すべき機能
class PrivacyFilter:
def __init__(self):
self.sensitive_keywords = [
'password', 'credit card', '口座番号', 'マイナンバー'
]
def mask_sensitive_data(self, text):
"""
機密情報のマスキング
"""
for keyword in self.sensitive_keywords:
if keyword in text.lower():
return '[機密情報を含むため非表示]'
return text
def blur_face(self, frame, faces):
"""
顔部分をぼかし処理
"""
for (x, y, w, h) in faces:
face_region = frame[y:y+h, x:x+w]
blurred = cv2.GaussianBlur(face_region, (99, 99), 30)
frame[y:y+h, x:x+w] = blurred
return frame
運用ルール
- データ保存期間の制限(例:7日間)
- 監視の一時停止機能
- 従業員への明示的な同意取得
- ローカル処理(クラウドアップロードなし)
まとめ
本記事では、LMMに頼らず、OCR + OpenCVで低コストな作業ログシステムを構築する方法を紹介しました。
メリット
- 圧倒的な低コスト(月額 $0 vs LMMの $19.2)
- オフライン動作可能(プライバシー保護)
- カスタマイズ性が高い
デメリット
- LMMに比べて精度は劣る
- 複雑な文脈理解は困難
推奨アプローチ
ハイブリッド方式で、99%のコスト削減と高精度を両立させましょう!
参考リンク
次のステップ:
- MediaPipeでの姿勢推定追加
- Dashboardの作成(Flask + Chart.js)
- 機械学習モデルでの作業分類精度向上
ぜひ、あなたの環境でも試してみてください!