【Java】OpenCVでUSBカメラでキャプチャした画像を保存する

# 開発環境
・Windows 10 Home
・Eclipse Version: 4.25.0
・Java SE-17
・OpenCV Version 4.6.0

# 実現したい処理、機能
・PCに接続したUSBカメラで一定の時間ごとに画像をキャプチャし、時刻と撮影番号を追加し、指定したフォルダに保存する。
・GUI上に撮影開始、終了ボタンを設置し、ボタン押下で撮影制御をできるようにする。
・GUI上で解像度、撮影間隔(分)を設定できるようにする。
・GUI上に撮影した画像のプレビューを表示する。

# OpenCVのインストール
■参考資料
・ダウンロード
https://opencv.org/releases/

・Windows に OpenCV をインストールし JAR ファイルを生成する
https://neos21.net/blog/2020/06/03-01.html

・EclipseでJava用のOpenCVを使う(for windows)
https://qiita.com/livlea/items/a853c374d6d91b33f5fe

# サンプルコード
```java:CameraCapture.java
//import文省略

public class CameraCapture extends JFrame {
    // フレームの幅を示す定数
    private static final int FRAME_WIDTH = 640;
    // フレームの高さを示す定数
    private static final int FRAME_HEIGHT = 480;
    // 撮影間隔を示す定数。デフォルトでは1分
    private static final int CAPTURE_INTERVAL_MINUTES = 1;
    // テキストの色を示す定数
    private static final Scalar FONT_COLOR = new Scalar(255, 255, 255);
    // テキストの太さを示す定数
    private static final int FONT_THICKNESS = 2;

    // VideoCaptureオブジェクト カメラデバイスからのフレームのキャプチャを行う
    private VideoCapture videoCapture;
    // Timerオブジェクト 撮影間隔ごとにフレームの取得と保存を行う
    private Timer captureTimer;
    // 撮影番号を示すカウンター変数
    private int captureCounter;

    // 撮影開始ボタンのJButtonオブジェクト
    private JButton startButton;
    // 撮影終了ボタンのJButtonオブジェクト
    private JButton stopButton;
    // ステータスを表示するJLabelオブジェクト
    private JLabel statusLabel;
    // プレビューを表示するJLabelオブジェクト
    private JLabel previewLabel;
    // 解像度を選択するJComboBoxオブジェクト
    private JComboBox<String> resolutionComboBox;
    // 撮影間隔を入力するJTextFieldオブジェクト
    private JTextField intervalTextField;

    // サポートされる解像度の配列
    private Dimension[] resolutions = {
            new Dimension(640, 480),
            new Dimension(1280, 720),
            new Dimension(1920, 1080)
    };

    // コンストラクタ GUIウィンドウの初期化と構築を行う
    public CameraCapture() {
        setTitle("CameraCapture");
        setSize(FRAME_WIDTH, FRAME_HEIGHT);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);

        startButton = new JButton("撮影開始");
        stopButton = new JButton("撮影終了");
        statusLabel = new JLabel("Status: Not Started");
        previewLabel = new JLabel();
        resolutionComboBox = new JComboBox<>();
        intervalTextField = new JTextField(String.valueOf(CAPTURE_INTERVAL_MINUTES));

        for (Dimension resolution : resolutions) {
            resolutionComboBox.addItem(resolution.width + "x" + resolution.height);
        }
        resolutionComboBox.setSelectedIndex(0);

        // 撮影開始ボタンのアクションリスナー
        startButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                startCapture();
                updateButtonStates(true);
                statusLabel.setText("Status: Started");
            }
        });

        // 撮影終了ボタンのアクションリスナー
        stopButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                stopCapture();
                updateButtonStates(false);
                statusLabel.setText("Status: Stopped");
            }
        });

        JPanel buttonPanel = new JPanel();
        buttonPanel.add(startButton);
        buttonPanel.add(stopButton);

        JPanel settingPanel = new JPanel();
        settingPanel.add(new JLabel("解像度:"));
        settingPanel.add(resolutionComboBox);
        settingPanel.add(new JLabel("撮影間隔（分）:"));
        settingPanel.add(intervalTextField);

        setLayout(new BorderLayout());
        add(buttonPanel, BorderLayout.CENTER);
        add(statusLabel, BorderLayout.SOUTH);
        add(previewLabel, BorderLayout.NORTH);
        add(settingPanel, BorderLayout.EAST);
    }

    // 撮影を開始するメソッド
    private void startCapture() {
        // 解像度を取得
        Dimension resolution = resolutions[resolutionComboBox.getSelectedIndex()];
        // 撮影間隔を取得
        int captureInterval = Integer.parseInt(intervalTextField.getText()) * 60 * 1000;

        videoCapture = new VideoCapture(0); // 0はカメラデバイスのIDで、複数のカメラが接続されている場合に適宜変更する

        // カメラデバイスのオープンに成功しているかどうかをチェック
        if (videoCapture.isOpened()) {
            videoCapture.set(3, resolution.getWidth()); // Width
            videoCapture.set(4, resolution.getHeight()); // Height
            captureCounter = 1;

            // タイマータスクを作成して指定の間隔で撮影を行う
            captureTimer = new Timer();
            captureTimer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    Mat frame = new Mat();
                    videoCapture.read(frame);

                    // 画像に時刻と撮影番号を挿入
                    addTimestampText(frame);

                    // 画像を保存
                    saveImage(frame);

                    // プレビューを更新
                    updatePreview(frame);

                    captureCounter++;
                }
            }, 0, captureInterval);
        } else {
            JOptionPane.showMessageDialog(this, "Failed to open camera.", "Error", JOptionPane.ERROR_MESSAGE);
        }
    }

    // 撮影を停止するメソッド
    private void stopCapture() {
        if (captureTimer != null) {
            captureTimer.cancel();
            captureTimer = null;
        }

        if (videoCapture != null && videoCapture.isOpened()) {
            videoCapture.release();
            videoCapture = null;
        }
    }

    // フレームに時刻と撮影番号を追加するメソッド
    private void addTimestampText(Mat frame) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
        String timestamp = dateFormat.format(new Date());
        String text = timestamp + "_" + captureCounter;

        int baseline[] = {0};
        Size textSize = Imgproc.getTextSize(text, Imgproc.FONT_HERSHEY_SIMPLEX, FONT_THICKNESS, FONT_THICKNESS, baseline);
        org.opencv.core.Point textOrg = new org.opencv.core.Point(
                frame.cols() - textSize.width - 10,
                frame.rows() - 10);

        Imgproc.putText(
                frame,
                text,
                textOrg,
                Imgproc.FONT_HERSHEY_SIMPLEX,
                FONT_THICKNESS,
                FONT_COLOR,
                FONT_THICKNESS);
    }

    // 画像を保存するメソッド
    private void saveImage(Mat frame) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
        String timestamp = dateFormat.format(new Date());
        String fileName = timestamp + "_" + captureCounter + ".jpg";
        String outputPath = "保存先のフォルダパス" + fileName; // 保存先のフォルダパスを適宜変更する

        Imgcodecs.imwrite(outputPath, frame);
    }

    // プレビューを更新するメソッド
    private void updatePreview(Mat frame) {
        BufferedImage image = matToBufferedImage(frame);
        ImageIcon icon = new ImageIcon(image.getScaledInstance(FRAME_WIDTH, FRAME_HEIGHT, Image.SCALE_DEFAULT));
        previewLabel.setIcon(icon);
    }

    // MatオブジェクトをBufferedImageに変換するメソッド
    private BufferedImage matToBufferedImage(Mat frame) {
        int type = BufferedImage.TYPE_BYTE_GRAY;
        if (frame.channels() > 1) {
            type = BufferedImage.TYPE_3BYTE_BGR;
        }
        BufferedImage image = new BufferedImage(frame.cols(), frame.rows(), type);
        frame.get(0, 0, ((DataBufferByte) image.getRaster().getDataBuffer()).getData());
        return image;
    }

    // ボタンの状態を更新するメソッド
    private void updateButtonStates(boolean capturing) {
        startButton.setEnabled(!capturing);
        stopButton.setEnabled(capturing);
        resolutionComboBox.setEnabled(!capturing);
        intervalTextField.setEditable(!capturing);
    }

    // メインメソッド
    public static void main(String[] args) {
        // OpenCVの初期化
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                CameraCapture gui = new CameraCapture();
                gui.setVisible(true);
            }
        });
    }
}

```

# 今後実装したい機能
・動画形式でカメラプレビューを表示する。
・解像度に合わせて、挿入する時刻と撮影番号の大きさを調整する。
・保存先をGUI上で指定できるようにする。

以上