じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

Rustでスクリーンショットを撮影してOpenCVで画像処理を行う方法と依存せずに使う方法

はじめに

MacBookでRustを使ってスクリーンショットを撮る方法について紹介します。この記事では、次の2つのアプローチを解説します:

  1. OpenCVを活用する方法 — 画像処理の多彩な機能を利用
  2. 外部ライブラリに依存しない方法 — シンプルながら効果的

それぞれのアプローチには利点と課題があります。詳細なコード例を交えながら、最終的にはSlackウィンドウを自動検出する実用的なテクニックまでご紹介します。

最近ではマルチモーダルAIの発展により、AIシステムもスクリーンショットの取得と分析を行うことが増えています。生成AIが画面の視覚情報を理解し、より的確な支援を提供するためには、高品質なスクリーンショット機能が不可欠です。本記事で紹介する技術は、そうしたAIシステムの視覚的入力にも応用できるでしょう。

目次

使用するクレート

今回使用する主なクレートは以下の通りです:

macOS環境のセットアップ

MacBookスクリーンショット処理を行うための環境構築について解説します。

OpenCVのインストール(OpenCVアプローチの場合)

Homebrewを使って簡単にOpenCVをインストールできます:

brew install opencv

LLVMとClangのインストール(OpenCVアプローチの場合)

opencv-rustクレートはバインディング生成にlibclangを使用しています:

brew install llvm

環境変数の設定(OpenCVアプローチの場合)

OpenCVLLVMを正しく検出するために、以下の環境変数を設定します。これらの設定は.zshrc.bash_profileに追加しておくと便利です:

# OpenCV設定
export OPENCV_LINK_LIBS="opencv_core,opencv_imgproc,opencv_highgui,opencv_videoio"
export OPENCV_LINK_PATHS="/opt/homebrew/lib"  # Apple Siliconの場合
export OPENCV_INCLUDE_PATHS="/opt/homebrew/include/opencv4"

# LLVM/Clang設定
export LIBCLANG_PATH=$(brew --prefix llvm)/lib
export DYLD_LIBRARY_PATH=$(brew --prefix llvm)/lib:$DYLD_LIBRARY_PATH

注意: パスはApple Siliconモデルの場合です。Intel Macでは異なる場合があります。 brew --prefix opencv コマンドで確認できます。

XCapのみのセットアップ(シンプルアプローチ)

OpenCVを使わない場合は、xcapクレートだけをインストールします:

cargo add xcap

Cargo.tomlの設定

プロジェクトのCargo.tomlファイルは以下のようになります:

OpenCVアプローチの場合

[dependencies]
xcap = "0.0.4"
opencv = { version = "0.94.4", features = ["clang-runtime"] }

OpenCVに依存しないアプローチの場合

[dependencies]
xcap = "0.0.4"

OpenCVアプローチ:基本的なスクリーンショット処理

OpenCVを使ったスクリーンショット処理の基本的なコードを紹介します:

use std::time::Instant;
use xcap::Monitor;
use opencv::prelude::*;
use opencv::core::{Mat, Size, CV_8UC4};
use opencv::imgproc;
use opencv::highgui;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // OpenCVのウィンドウを作成
    highgui::named_window("Screenshot", highgui::WINDOW_AUTOSIZE)?;
    highgui::named_window("Processed", highgui::WINDOW_AUTOSIZE)?;

    println!("Press 'q' to exit");

    // メインループ
    loop {
        let start = Instant::now();

        // プライマリモニターを取得
        let monitors = Monitor::all()?;
        let primary_monitor = monitors.iter().find(|m| m.is_primary().unwrap_or(false))
            .unwrap_or(&monitors[0]);

        // スクリーンショットを撮影
        let image = primary_monitor.capture_image()?;
        let width = image.width() as i32;
        let height = image.height() as i32;
        
        // ピクセルデータを取得
        let raw_pixels = image.as_raw();

        // OpenCVのMat形式に変換
        let mat = unsafe {
            let mut mat = Mat::new_size(Size::new(width, height), CV_8UC4)?;
            let mat_data = mat.data_mut();
            std::ptr::copy_nonoverlapping(
                raw_pixels.as_ptr(),
                mat_data,
                (width * height * 4) as usize
            );
            mat
        };

        // 元のスクリーンショットを表示
        highgui::imshow("Screenshot", &mat)?;

        // 画像処理の例: グレースケール変換
        let mut gray = Mat::default();
        imgproc::cvt_color(
            &mat, 
            &mut gray, 
            imgproc::COLOR_BGRA2GRAY, 
            0, 
            opencv::core::AlgorithmHint::ALGO_HINT_DEFAULT
        )?;

        // エッジ検出の例
        let mut edges = Mat::default();
        imgproc::canny(&gray, &mut edges, 100.0, 200.0, 3, false)?;

        // 処理した画像を表示
        highgui::imshow("Processed", &edges)?;

        // 処理時間を表示
        println!("処理時間: {:?}", start.elapsed());

        // キー入力を待つ(10ms)
        let key = highgui::wait_key(10)?;
        if key == 'q' as i32 || key == 'Q' as i32 {
            break;
        }
    }

    Ok(())
}

このコードは以下のことを行います:

  1. XCapを使ってプライマリモニターのスクリーンショットを撮影
  2. スクリーンショットのデータをOpenCVのMat形式に変換
  3. 元のスクリーンショットを表示し、グレースケール変換とエッジ検出を適用した処理結果も表示

OpenCVを使う大きなメリットは、豊富な画像処理機能を利用できることです。グレースケール変換、エッジ検出、顔認識など多様な処理が可能です。

MacOSでの画像保存の問題と解決策

MacOSOpenCVimwriteimencode関数を使用すると、リンクエラーが発生することがあります。以下のカスタム関数を使用して回避できます:

// MacOS環境のためのOpenCVラッパー関数
fn save_image(filename: &str, img: &Mat) -> Result<bool, Box<dyn std::error::Error>> {
    // Rustのファイル操作を使用してOpenCVのMatをPNGとして保存
    println!("画像を保存しています: {}", filename);
    
    // エンコード用のベクタ
    let mut buf = opencv::core::Vector::new();
    
    // BGR形式の画像をPNGにエンコード
    opencv::imgcodecs::imencode(".png", img, &mut buf, &opencv::core::Vector::new())?;
    
    // ファイルに書き込み
    fs::write(filename, buf.as_slice())?;
    
    Ok(true)
}

しかし、この関数もOpenCVのバージョンやMacOSの設定によってはエラーになる場合があります。その場合は次に説明するOpenCVに依存しないアプローチを検討することをお勧めします。

OpenCVに依存しないアプローチ

OpenCVのリンクエラーや複雑な設定を避けたい場合は、XCapクレートのみを使用したシンプルなアプローチも可能です:

use std::time::Instant;
use std::fs;
use xcap::Monitor;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("スクリーンショットプログラムを開始しました");
    println!("終了するには Ctrl+C を押してください");

    // メインループ
    loop {
        let start = Instant::now();

        // プライマリモニターを取得
        let monitors = Monitor::all()?;
        let primary_monitor = monitors.iter().find(|m| m.is_primary().unwrap_or(false))
            .unwrap_or(&monitors[0]);

        // スクリーンショットを撮影
        let image = primary_monitor.capture_image()?;
        
        // スクリーンショットを保存
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::SystemTime::UNIX_EPOCH)?
            .as_secs();
        let filename = format!("screenshot_{}.png", timestamp);
        
        // XCapのsaveメソッドを使用して直接保存
        image.save(&filename)?;
        
        println!("スクリーンショットを保存しました: {}", filename);
        println!("処理時間: {:?}", start.elapsed());
        
        // 適当な間隔を空ける
        std::thread::sleep(std::time::Duration::from_secs(5));
    }

    Ok(())
}

このアプローチのメリットは:

  1. セットアップが格段に簡単(OpenCVLLVMのインストールが不要)
  2. リンクエラーなどのトラブルが少ない
  3. 軽量で高速

一方、デメリットは:

  1. 高度な画像処理機能が使えない
  2. 独自の画像解析ロジックを実装する必要がある

簡易な画像解析を実装する

OpenCVを使わずに簡易な画像解析を行う例として、特定の色を検出するコードを示します:

// 簡易な色検出機能
fn detect_color(rgba_data: &[u8], width: u32, height: u32) -> bool {
    // 特定の色の範囲(RGB値)
    let target_lower_r = 200;
    let target_lower_g = 0;
    let target_lower_b = 0;
    
    let target_upper_r = 255;
    let target_upper_g = 100;
    let target_upper_b = 100;
    
    let mut target_pixel_count = 0;
    let total_pixels = (width * height) as usize;
    
    // ピクセルデータを4バイトずつ処理(RGBA)
    for i in (0..rgba_data.len()).step_by(4) {
        if i + 2 < rgba_data.len() {
            let r = rgba_data[i];
            let g = rgba_data[i + 1];
            let b = rgba_data[i + 2];
            
            // 指定した範囲内の色かどうかを判定
            if r >= target_lower_r && r <= target_upper_r &&
               g >= target_lower_g && g <= target_upper_g &&
               b >= target_lower_b && b <= target_upper_b {
                target_pixel_count += 1;
            }
        }
    }
    
    // 閾値: 特定の色のピクセルが一定数以上あれば検出成功
    let threshold_ratio = 0.01; // 全ピクセルの1%以上
    let has_enough_pixels = (target_pixel_count as f64 / total_pixels as f64) > threshold_ratio;
    
    has_enough_pixels
}

このコードはRGBA値を直接処理して、指定した色範囲のピクセル数をカウントします。単純ですが、特定の色を持つUIエレメントの検出などには十分な場合があります。

実用例:Slackスクリーンショットモニター

参考的な例として、Slackウィンドウを自動検出してスクリーンショットを保存するアプリケーションを作ってみましょう。以下では、OpenCVに依存しないシンプルなバージョンを紹介します:

use std::time::{Instant, Duration, SystemTime};
use std::fs;
use std::path::Path;
use std::thread;
use xcap::Monitor;

// スクリーンショット撮影の設定
const SCREENSHOT_INTERVAL: u64 = 5; // 5秒ごとにスクリーンショットを撮影
const SAVE_PATH: &str = "slack_screenshots";

// 簡易なSlackウィンドウ検出機能
fn detect_slack_window(rgba_data: &[u8], width: u32, height: u32) -> bool {
    // Slackの紫色の範囲(RGB値)
    let purple_lower_r = 100;
    let purple_lower_g = 50;
    let purple_lower_b = 130;
    
    let purple_upper_r = 170;
    let purple_upper_g = 100;
    let purple_upper_b = 210;
    
    let mut purple_pixel_count = 0;
    let total_pixels = (width * height) as usize;
    
    // ピクセルデータを4バイトずつ処理(RGBA)
    for i in (0..rgba_data.len()).step_by(4) {
        if i + 2 < rgba_data.len() {
            let r = rgba_data[i];
            let g = rgba_data[i + 1];
            let b = rgba_data[i + 2];
            
            // 指定した範囲内の紫色かどうかを判定
            if r >= purple_lower_r && r <= purple_upper_r &&
               g >= purple_lower_g && g <= purple_upper_g &&
               b >= purple_lower_b && b <= purple_upper_b {
                purple_pixel_count += 1;
            }
        }
    }
    
    // 閾値: 紫色のピクセルが一定数以上あればSlackウィンドウと判断
    let threshold_ratio = 0.001; // 全ピクセルの0.1%以上が紫色
    let has_enough_purple = (purple_pixel_count as f64 / total_pixels as f64) > threshold_ratio;
    
    // デバッグ用(閾値調整に便利)
    println!("紫色ピクセル数: {}, 全ピクセル数: {}, 比率: {:.6}", 
        purple_pixel_count, total_pixels, purple_pixel_count as f64 / total_pixels as f64);
    
    has_enough_purple
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 保存用ディレクトリの作成
    if !Path::new(SAVE_PATH).exists() {
        fs::create_dir(SAVE_PATH)?;
    }

    println!("Slackスクリーンショットモニタリングを開始しました");
    println!("スクリーンショットは{}ディレクトリに保存されます", SAVE_PATH);
    println!("終了するには Ctrl+C を押してください");

    let mut last_saved_time = Instant::now() - Duration::from_secs(SCREENSHOT_INTERVAL);
    let mut screenshot_count = 0;

    // メインループ
    loop {
        let current_time = Instant::now();
        
        // 指定した間隔が経過したらスクリーンショットを撮影
        if current_time.duration_since(last_saved_time).as_secs() >= SCREENSHOT_INTERVAL {
            last_saved_time = current_time;
            
            // すべてのモニターを取得
            let monitors = Monitor::all()?;
            let primary_monitor = monitors.iter().find(|m| m.is_primary().unwrap_or(false))
                .unwrap_or(&monitors[0]);
            
            // スクリーンショットを撮影
            let image = primary_monitor.capture_image()?;
            let width = image.width();
            let height = image.height();
            
            // XCapのImageからRGBAデータを取得
            let rgba_data = image.as_raw();
            
            // Slackウィンドウの検出
            if detect_slack_window(rgba_data, width, height) {
                // スクリーンショットを保存
                let timestamp = SystemTime::now()
                    .duration_since(SystemTime::UNIX_EPOCH)?
                    .as_secs();
                let filename = format!("{}/slack_screenshot_{}.png", SAVE_PATH, timestamp);
                
                // XCapのsaveメソッドを使用して直接保存
                image.save(&filename)?;
                
                println!("Slackウィンドウを検出しました。スクリーンショットを保存: {}", filename);
                screenshot_count += 1;
            }
        }
        
        // CPUの負荷を下げるためのスリープ
        thread::sleep(Duration::from_millis(500));
    }
}

このアプリケーションは:

  1. 定期的にスクリーンショットを撮影
  2. 画面上にSlackの特徴的な紫色が一定量以上あるかを検出
  3. Slackウィンドウと判断された場合、スクリーンショットを保存

トラブルシューティング

MacBookでRustとOpenCVを使う際によく遭遇する問題と解決法をまとめます。

1. libclang.dylibが見つからない場合

エラーメッセージ:

dyld: Library not loaded: @rpath/libclang.dylib

解決策:

brew install llvm
export LIBCLANG_PATH=$(brew --prefix llvm)/lib
export DYLD_LIBRARY_PATH=$(brew --prefix llvm)/lib:$DYLD_LIBRARY_PATH

2. OpenCVのライブラリが見つからない場合

エラーメッセージ:

Failed to find installed OpenCV package

解決策: 正しいパスを環境変数に設定します:

export OPENCV_LINK_LIBS="opencv_core,opencv_imgproc,opencv_highgui,opencv_videoio"
export OPENCV_LINK_PATHS="/opt/homebrew/lib"  # Apple Siliconの場合
export OPENCV_INCLUDE_PATHS="/opt/homebrew/include/opencv4"

3. リンクエラー: imwrite, imencodeなどの関数が見つからない

エラーメッセージ:

Undefined symbols for architecture arm64: "cv::imwrite..."

解決策: 1. OpenCVを完全に再インストールしてみる: bash brew uninstall --ignore-dependencies opencv brew install opencv

  1. それでも解決しない場合は、OpenCVに依存しないアプローチに切り替える

まとめ

この記事では、MacBook環境でRustを使ってスクリーンショットを撮影し処理する2つのアプローチを紹介しました。

  1. OpenCVを使ったアプローチ

    • メリット:高度な画像処理機能が使える
    • デメリット:セットアップが複雑、リンク問題が発生することがある
  2. OpenCVに依存しないアプローチ

    • メリット:シンプルで信頼性が高い、セットアップが容易
    • デメリット:高度な画像処理機能を自分で実装する必要がある

それぞれのアプローチにはメリット・デメリットがありますが、用途に応じて適切な方法を選択することで、Rustの安全性と高パフォーマンスを活かした画像処理アプリケーションを開発できます。

実用例として紹介したSlackスクリーンショットモニターは、このようなスクリーンショット処理の応用例の一つです。この基本的なアプローチを発展させて、画面録画ツール、監視アプリケーション、自動化ツールなど、様々な実用的なアプリケーションを開発することができます。

参考リンク