JavaCVは、画像処理ライブラリのOpenCVと動画・音声処理のFFmpegを、Javaから呼び出せるようにするオープンソースのライブラリです。C/C++のネイティブライブラリをJARに同梱しているため、OSにOpenCVやFFmpegをインストールしなくても、Mavenに依存を1つ追加すれば動画の読み込みや画像処理を始められます。
この記事では、2026年8月10日公開の最新版JavaCV 1.5.14を対象に、構成、導入、動画の読み書き、OpenCVでの画像処理、よく出るエラーまでを順に解説します。コードはmacOS(x86_64)上のEclipse Temurin 25とMaven 3.9.16で実行し、出力をそのまま載せています(Gradleの設定例とWebカメラの節は未実行です)。
まとめ:JavaCVの要点と1.5.14の構成
- JavaCVは、ネイティブライブラリをJavaに橋渡しするJavaCPP、ライブラリごとの橋渡し定義であるJavaCPP Presets、その上の便利クラス群、の3層でできています。普段書くのは
FFmpegFrameGrabber、FFmpegFrameRecorder、Frame、FrameConverterなどの最上層のクラスです。 - 1.5.14に同梱される版はOpenCV 4.14.0、FFmpeg 8.1.2、Tesseract 5.5.3、OpenBLAS 0.3.34です。OpenCV 5.0やFFmpeg 9.0はまだ入っていません。
- 動作要件はJava SE 8以降、Androidは7.0以降です。ライセンスはApache License 2.0とGPL(Classpath例外付き)の選択制で、商用プロジェクトでも使えます。
javacv-platformをそのまま入れると全OS分のネイティブライブラリが入り、今回の計測では139個のJAR・約736MBでした。-Djavacpp.platform=macosx-x86_64で絞ると46個・約98MBになります。pom.xmlの<properties>に書いただけでは絞れません。- 標準の
ffmpeg-platformのH.264エンコーダーはOpenH264で、crfは無視されます。libx264を使うにはffmpeg-platform-gplに差し替えます(GPLが適用されます)。 grabImage()が返すFrameは毎回同じオブジェクトです。リストに貯めるならclone()が必要です。grab()は音声フレームも返すため、映像だけ欲しいときはgrabImage()を使います。- 1.4.4以前の記事にある
org.bytedeco.javacpp.opencv_coreは、1.5でorg.bytedeco.opencv.opencv_core(クラス)とorg.bytedeco.opencv.global.opencv_core(関数)に分かれました。古いコードは1.5.14ではコンパイルできません。
JavaCVの3層構造:JavaCPP・Presets・JavaCVの役割分担
JavaCVという名前は最上層のライブラリを指しますが、Mavenで入るのは3層分のJARです。どの層が何をしているかを押さえておくと、依存サイズやエラーの原因を追いやすくなります。
| 層 | 主な成果物 | 役割 |
|---|---|---|
| JavaCPP | javacpp |
JNIの生成とネイティブライブラリの読み込み |
| JavaCPP Presets | opencv、ffmpeg など |
C/C++のAPIをほぼそのままJavaに写したもの |
| JavaCV | javacv |
Frame・Grabber・Recorderなどの便利クラス |
Presets層のJARは2種類に分かれます。opencv-4.14.0-1.5.14.jar のような分類子なしのJARにはJavaのクラスだけが入っており、opencv-4.14.0-1.5.14-macosx-x86_64.jar のようにOS名の付いたJARに .dylib や .dll などのネイティブライブラリが入っています。末尾に -platform が付く成果物(javacv-platform、opencv-platform など)は、このOS別JARをまとめて引き込むためのものです。
JavaCV層の中心は、入力を抽象化した FrameGrabber(ファイル、ストリーム、カメラ)、出力を抽象化した FrameRecorder、映像と音声を1つの型で運ぶ Frame、そして Frame をOpenCVの Mat やJavaの BufferedImage に変換する FrameConverter です。OpenCVやFFmpegのC APIを直接触らなくても、この4つで多くの処理が書けます。
JavaCVのREADMEは、OpenCVとFFmpegのほかにTesseract(OCR)、Leptonica、librealsense(Intel RealSense)、libfreenect(Kinect)、FlyCapture、ARToolKitPlusなども対象に挙げています。javacv-platform はこれらの多くを依存に含むため、次の章で見るように依存が大きくなります。
OpenCV公式Javaバインディング・JAVE2との比較と選び方
「JavaでOpenCVを使う」「JavaからFFmpegを使う」方法はJavaCVだけではありません。2026年10月時点の主な選択肢は次のとおりです。
| 方法 | 最新版 | 扱える範囲 | 導入 |
|---|---|---|---|
| JavaCV | 1.5.14(2026-08-10) | OpenCV+FFmpeg+他 | Maven Central |
| OpenCV公式Javaバインディング | OpenCV 5.0.0 | OpenCVのみ | Windowsは配布済みバイナリ、その他はソースビルド |
| OpenCV公式Android版 | 5.0.0.1(AAR) | OpenCVのみ | org.opencv:opencv |
| openpnp/opencv | 4.9.0-0(2023-12) | OpenCVのみ | Maven Central |
| JAVE2 | 4.2.0(2026-08-20) | FFmpegのみ | Maven Central |
OpenCVはビルド時にJavaバインディング(JARとJNIライブラリ)を生成できますが、デスクトップ向けのJARはMaven Centralに公式成果物がありません。Maven Centralにある公式の org.opencv:opencv はAndroid向けのAARです。デスクトップでMaven経由にしたい場合、以前はopenpnpのビルドがよく使われましたが、2023年12月の4.9.0-0で更新が止まっています。言語にこだわらずOpenCV 5.0を試したいだけなら、pipで入るPython版(opencv-python)のほうが早く始められます。使い方は「OpenCVとは?Pythonでの使い方入門」で解説しています。
JAVE2は、JARに同梱したFFmpegの実行ファイルを展開し、外部プロセスとして起動する方式です。「MP4をWebMに変換する」のようなファイル単位の変換なら、JNIを介さないJAVE2も候補です。ただしJAVE2本体はGPLv3なので、配布するアプリではライセンス条件も確認します。4.2.0の同梱FFmpegは9.0.1で、JavaCV 1.5.14の同梱版より新しい版です。
JavaCVを選ぶべきなのは、1フレームずつJavaのコードで画素を処理したい場合と、OpenCVとFFmpegを同じプログラムで組み合わせたい場合です。逆に、変換だけが目的ならJAVE2か ProcessBuilder でffmpegコマンドを呼ぶ方法を勧めます。ffmpegコマンドの書き方は「FFmpegとは?使い方とコマンド一覧」にまとめています。
JavaCVを入れてあるなら、ffmpegコマンドを別にインストールする必要もありません。ffmpeg-platform のOS別JARには ffmpeg と ffprobe の実行ファイルが入っており、JavaCPP PresetsのCHANGELOGによると1.5から Loader.load() で呼び出せます。次のコードで、後述の FFmpegFrameRecorder で作る5秒のMP4をVP9のWebMに変換できました(終了コード0、出力121,126バイト)。
import org.bytedeco.javacpp.Loader;
public class BundledFfmpeg {
public static void main(String[] args) throws Exception {
// JARに同梱されたffmpegを展開し、実行ファイルのパスを受け取る
String ffmpeg = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
Process p = new ProcessBuilder(ffmpeg, "-y", "-hide_banner", "-loglevel", "error",
"-i", "sample.mp4", "-c:v", "libvpx-vp9", "-b:v", "300k", "out.webm")
.inheritIO()
.start();
System.out.println("exit=" + p.waitFor());
}
}
展開先は ~/.javacpp/cache/ の下で、同梱のffmpegは ffmpeg version 8.1.2 と表示されました。LGPL版のため、エンコーダーにlibx264は無く、libopenh264・libvpx・libaom・libopusなどが使えます。画像・動画処理を組み込んだ業務システムを一から作る場合は、フルスクラッチ開発のように要件に合わせて構成を決める進め方もあります。
Maven・Gradleでの導入とjavacv-platformの依存サイズ
Mavenの最小設定とjavacpp.platformの指定方法
必要な依存指定は javacv-platform の1つです。以下は既存のpom.xmlの <project> 内に追加する断片で、既存の <properties> と <dependencies> があれば、それぞれに内容を統合します。javacv だけを書くとネイティブライブラリが入らず、実行時に後述の UnsatisfiedLinkError になります。
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.14</version>
</dependency>
</dependencies>
この設定のまま依存を解決すると、Android、iOS、Linux、macOS、Windowsの全プラットフォーム分が入ります。JavaCV 1.5.14で実際に取得されたJARの数と合計サイズ(1MB=1,000,000バイト)を計測した結果が次の表です。OSを絞った行の値はmacosx-x86_64の場合で、OSによって多少変わります。
| 指定方法 | JAR数 | 合計サイズ |
|---|---|---|
| 指定なし | 139 | 約736MB |
pom.xmlの <properties> に記述 |
139 | 約736MB |
-Djavacpp.platform=macosx-x86_64 |
46 | 約98MB |
JavaCVのREADMEは、javacpp.platform を「システムプロパティとして -D オプションで」渡すよう書いています。-platform 成果物は、このシステムプロパティを条件に有効になるMavenプロファイルで依存を切り替えているため、pom.xmlの <properties> に同じ名前の値を書いても効きません。ビルドとCIのコマンドに -D を付けます。
# 全プラットフォーム分を取得(139 JAR・約736MB)
mvn dependency:build-classpath -Dmdep.outputFile=cp.txt
# macosx-x86_64だけに絞る(46 JAR・約98MB)
mvn dependency:build-classpath -Djavacpp.platform=macosx-x86_64 -Dmdep.outputFile=cp.txt
指定できる値は linux-x86_64、linux-arm64、macosx-x86_64、macosx-arm64、windows-x86_64、android-arm64 などです。Apple Silicon(M1以降)のMacなら macosx-arm64 を指定します。
Gradle JavaCPPプラグインによる対象OSの指定
Gradleには公式のGradle JavaCPPプラグインがあり、javacppPlatform で対象を指定します。カンマ区切りで複数のOSを並べられるため、開発機と本番サーバーのOSが違う場合に便利です。以下はGradle JavaCPPのREADMEの例をJavaCV 1.5.14に合わせたもので、手元では実行していません。
plugins {
id 'java'
id 'org.bytedeco.gradle-javacpp-platform' version '1.5.10'
}
// コマンドラインなら -PjavacppPlatform=linux-x86_64,windows-x86_64
ext {
javacppPlatform = 'linux-x86_64,windows-x86_64'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.bytedeco:javacv-platform:1.5.14'
}
このプラグインのリリースは2024年1月の1.5.10が最後で、JavaCV本体(1.5.14)と版番号がずれています。READMEの例は version "$javacppVersion" とJavaCVと同じ版を書く形になっているので、1.5.14と書くとプラグインが見つからずに失敗します。
javacv-platformの不要なネイティブ依存の削減
-Djavacpp.platform=macosx-x86_64 で絞っても、46個のJARの中にはARToolKitPlus、FlyCapture、libdc1394、libfreenect、librealsense、videoInputのJARが残ります。これらは1.5.9(2023年6月)の版で止まっており、OpenCVとFFmpegしか使わないなら不要です。JavaFXのJAR(javafx-base、javafx-graphics)も入ります。
依存を javacv、opencv-platform、ffmpeg-platform の3つに組み替えて同じ -D を付けると27個・約82MBまで減りました。ただし javacv 自体がこれらの成果物のJava部分に依存しているため、ネイティブライブラリが外れるだけで、JARの数はゼロにはなりません。配布サイズをさらに詰めたい場合は、Mavenの <exclusions> で個別に外し、実行して確かめます。
Mavenを使わないJARの入手:javacv-platformのbin.zip
MavenやGradleを使わない環境では、GitHubのリリースページからJARをまとめたアーカイブを取得できます。1.5.14の javacv-platform-1.5.14-bin.zip は1,302,297,073バイト(約1.3GB)で、READMEのとおりAndroid、iOS、Linux、macOS、Windowsの全OS分が入っています。展開したJARを必要なOSの分だけクラスパスに置けば動きますが、OSを絞る作業を手でやることになるため、ビルドツールが使えるなら前述の -Djavacpp.platform= を勧めます。
個別のJARはMaven Centralの org/bytedeco/ 以下にもあります。検索結果に出てくる com.googlecode.javacv は、プロジェクトがGoogle Codeにあった頃の旧groupIdです。Maven Centralの検索APIで引くと該当は0件で、Maven Centralにある最も古いJavaCVは org.bytedeco:javacv の0.8(2014年4月)でした。新しく書くpom.xmlに旧groupIdを使う理由はありません。
バージョン対応表:JavaCV 1.5.xと同梱OpenCV・FFmpeg
JavaCVの版を決めると、同梱されるOpenCVとFFmpegの版も決まります。公式のCHANGELOGから直近6版を抜き出しました。
| JavaCV | 公開日 | OpenCV | FFmpeg |
|---|---|---|---|
| 1.5.14 | 2026-08-10 | 4.14.0 | 8.1.2 |
| 1.5.13 | 2026-02-22 | 4.13.0 | 8.0.1 |
| 1.5.12 | 2025-06-30 | 4.11.0 | 7.1.1 |
| 1.5.11 | 2024-11-16 | 4.10.0 | 7.1 |
| 1.5.10 | 2024-01-29 | 4.9.0 | 6.1.1 |
| 1.5.9 | 2023-06-06 | 4.7.0 | 6.0 |
リリースはおおむね半年から1年に1回です。OpenCV 5.0.0(2026年6月)とFFmpeg 9.0(2026年10月時点の最新は9.0.2)はまだ同梱されていないため、これらの新機能が必要な場合はJavaCVの次の版を待つことになります。FFmpeg本体の版ごとの違いは「FFmpegの最新バージョンは9.0」で解説しています。
Presets層のJARは 4.14.0-1.5.14 のように「ライブラリの版-JavaCPPの版」という版番号を持ちます。opencv-platform などを個別に書く場合は、後半の 1.5.14 をJavaCVの版と揃えてください。揃っていないとJavaCPPの読み込み処理が合わず、実行時にエラーになります。
1.5.13では、FFmpegFrameGrabber・FFmpegFrameFilter・FFmpegFrameRecorder を閉じたときにLinuxでシステムメモリを返す処理が入りました。1.5.14はこの3クラスの変更はなく、依存の更新と、最低要件をJava SE 8・Android 7.0とする変更が中心です。
1.4以前のコードが動かない原因:groupIdとパッケージの変更
検索で見つかるJavaCVの記事には、2019年4月の1.5より前に書かれたものが多く残っています。1.5では、Presets層のgroupIdが org.bytedeco.javacpp-presets から org.bytedeco に変わりました(JavaCPP PresetsのCHANGELOGの「Rename groupId to “org.bytedeco”」)。同じ変更でPresetsはJPMSのモジュールに対応し、パッケージも組み替えられています。1.4.4と1.5.14のOpenCVのJARの中身を比べると、次のように移っています。
| 対象 | 1.4.4以前 | 1.5以降 |
|---|---|---|
| groupId | org.bytedeco.javacpp-presets |
org.bytedeco |
| Matクラス | org.bytedeco.javacpp.opencv_core.Mat |
org.bytedeco.opencv.opencv_core.Mat |
| 関数 | org.bytedeco.javacpp.opencv_imgproc |
org.bytedeco.opencv.global.opencv_imgproc |
旧形式の import static org.bytedeco.javacpp.opencv_core.*; を1.5.14の依存でコンパイルすると、次のエラーで止まります。
OldApi.java:1: error: cannot find symbol
import static org.bytedeco.javacpp.opencv_core.*;
^
symbol: class opencv_core
location: package org.bytedeco.javacpp
直し方は機械的で、クラス(Mat、Size など)は org.bytedeco.opencv.opencv_core から、関数(cvtColor、Canny など)は org.bytedeco.opencv.global.opencv_imgproc などから、それぞれimportし直します。FFmpegも同じで、org.bytedeco.javacpp.avcodec は org.bytedeco.ffmpeg.global.avcodec になります。org.bytedeco.javacv パッケージの FFmpegFrameGrabber などはそのままです。
FFmpegFrameGrabberによる動画のフレーム画像抽出
動画を1秒ごとのPNG画像に分割するコードです。FFmpegFrameGrabber で動画を開き、setTimestamp() でマイクロ秒単位の位置へ移動してから grabImage() で映像フレームを1枚取り出します。Java2DFrameConverter で BufferedImage に変換すれば、あとは標準の ImageIO で保存できます。
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import javax.imageio.ImageIO;
import java.io.File;
public class ExtractFrames {
public static void main(String[] args) throws Exception {
File outDir = new File("frames");
outDir.mkdirs();
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber("sample.mp4");
Java2DFrameConverter conv = new Java2DFrameConverter()) {
grabber.start();
System.out.printf("%dx%d %.2ffps frames=%d length=%dus codec=%s%n",
grabber.getImageWidth(), grabber.getImageHeight(), grabber.getFrameRate(),
grabber.getLengthInFrames(), grabber.getLengthInTime(), grabber.getVideoCodecName());
long step = 1_000_000L; // 1秒ごと(マイクロ秒)
for (long t = 0; t < grabber.getLengthInTime(); t += step) {
grabber.setTimestamp(t);
Frame frame = grabber.grabImage();
if (frame == null) break;
File out = new File(outDir, String.format("frame_%03d.png", t / step));
ImageIO.write(conv.convert(frame), "png", out);
System.out.println(out + " ts=" + grabber.getTimestamp());
}
grabber.stop();
}
}
}
5秒・30fps・640×360のH.264動画(後述の FFmpegFrameRecorder で作成)に対して実行した結果です。getLengthInTime() もマイクロ秒単位で返ります。
640x360 30.00fps frames=150 length=5000000us codec=h264
frames/frame_000.png ts=0
frames/frame_001.png ts=1000000
frames/frame_002.png ts=2000000
frames/frame_003.png ts=3000000
frames/frame_004.png ts=4000000
コードを ExtractFrames.java として保存し、前の章で出力した cp.txt をクラスパスにしてコンパイル・実行します。Java 24以降で起動する場合は --enable-native-access=ALL-UNNAMED を付けます(理由は後述)。この記事の他のコードも同じ手順で実行しています。このコマンドはmacOS・Linux向けです。Windowsではクラスパスの区切りをセミコロンに変更してください。画面を使わない実行環境では、動画生成・画像保存の例に -Djava.awt.headless=true を追加します。
javac -cp "$(cat cp.txt)" -d classes ExtractFrames.java
java --enable-native-access=ALL-UNNAMED -cp "classes:$(cat cp.txt)" ExtractFrames
setTimestampとsetVideoTimestampのシーク精度
引数1つの setTimestamp(long) は、FFmpegFrameGrabberのJavadocで「フレームの中身を確かめない(1.4.1より前の)旧方式のシーク」と説明されています。映像フレームを対象に中身を確かめながらシークするのが setVideoTimestamp(long) です。前述の5秒のsample.mp4と、音声付きの2秒のav.mp4(次の章で詳しく扱う、同梱ffmpegの testsrc と sine で作ったMPEG-4映像)で、1.0秒と1.5秒にシークしてから grabImage() したフレームの timestamp を比べました。
| 動画 | メソッド | 1.0秒を指定 | 1.5秒を指定 |
|---|---|---|---|
| sample.mp4(キーフレーム1枚) | setTimestamp |
1,000,000 | 1,500,000 |
| sample.mp4(キーフレーム1枚) | setVideoTimestamp |
966,666 | 1,466,666 |
| av.mp4(12枚ごとにキーフレーム) | setTimestamp |
800,000 | 1,200,000 |
| av.mp4(12枚ごとにキーフレーム) | setVideoTimestamp |
966,666 | 1,466,666 |
旧方式の setTimestamp は、av.mp4では指定より前のキーフレーム(0.8秒・1.2秒)に止まり、最大で0.3秒ずれました。setVideoTimestamp はどちらの動画でも指定時刻の1フレーム手前(30fpsで33,334マイクロ秒前)に止まりました。秒単位で位置を合わせたいなら setVideoTimestamp を使い、フレーム単位で合わせる必要があるときは、取り出したフレームの timestamp が指定時刻に届くまで grabImage() を繰り返します。
grabImageのFrame再利用とcloneによる複製
grabImage() と grab() が返す Frame は、呼ぶたびに新しく作られるわけではありません。3回取り出したフレームをそのままリストに入れたものと、clone() してから入れたものを比べると、次のようになりました。
same object: true
raw ts: 66666,66666,66666
clone ts: 0,33333,66666
そのまま入れた3つは同一のオブジェクトで、タイムスタンプはすべて最後のフレームの値(66,666マイクロ秒)に上書きされています。画素データも同じバッファを指しているため、後で処理すると最後の1枚が3回処理されます。フレームを溜めてから処理する場合は、次のように複製します。
List<Frame> frames = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Frame f = grabber.grabImage();
if (f == null) break; // 動画の終端
frames.add(f.clone()); // clone()しないと3要素とも同じオブジェクトになる
}
// 使い終わったら解放する
for (Frame f : frames) f.close();
Frameクラスのフィールドとgrab()が返す音声フレーム
Frame は、映像の1枚・音声の1区切り・字幕などのデータを同じ型で運ぶクラスです。1.5.14のタグのFrame.javaのソースで定義を確認すると、主なフィールドは次のとおりで、いずれもpublicのフィールドとして直接読めます。
| フィールド | 型 | 中身 |
|---|---|---|
image |
Buffer[] |
映像の画素(音声フレームではnull) |
imageWidth ほか |
int |
幅・高さ・深度・チャンネル数・1行の要素数 |
samples |
Buffer[] |
音声サンプル(映像フレームではnull) |
sampleRate、audioChannels |
int |
サンプリング周波数とチャンネル数 |
timestamp |
long |
マイクロ秒単位の時刻 |
keyFrame |
boolean |
キーフレームかどうか |
pictType |
char |
ピクチャタイプ('I'、'P'、'B') |
streamIndex |
int |
元のファイルでのストリーム番号 |
opaque |
Object |
元になったAVFrameやMatなど |
imageDepth には Frame.DEPTH_UBYTE(8)や Frame.DEPTH_FLOAT(32)などの定数が入ります。値はビット数で、DEPTH_BYTE(-8)のように負の値は符号付きを表します。動画から取った映像フレームは、BGRの DEPTH_UBYTE・3チャンネルになります。getTypes() を呼ぶと、そのフレームが映像・音声・データのどれを持っているかを EnumSet<Frame.Type> で返します。
注意が要るのは、grab() が映像だけでなく音声のフレームも順に返すことです。2秒・30fpsのMPEG-4映像と48kHzステレオのAAC音声を持つMP4(前述の同梱ffmpegの testsrc と sine で作成したav.mp4)を、grab() で最後まで読んで数えました(importは org.bytedeco.javacv.*)。
try (FFmpegFrameGrabber g = new FFmpegFrameGrabber("av.mp4")) {
g.start();
int video = 0, audio = 0, keys = 0, n = 0, firstVideo = -1;
Frame f;
while ((f = g.grab()) != null) {
if (f.image != null) {
if (firstVideo < 0) firstVideo = n;
video++;
if (f.keyFrame) keys++;
}
if (f.samples != null) audio++;
n++;
}
System.out.printf("grab total=%d video=%d (key=%d) audio=%d firstVideo=#%d%n",
n, video, keys, audio, firstVideo);
g.stop();
}
grab total=154 video=60 (key=5) audio=94 firstVideo=#24
154回のうち94回は音声フレームで、最初の映像フレームが返ったのは25回目でした。最初の grab() で返った音声フレームを Java2DFrameConverter に渡すと、戻り値は null でした。映像だけを処理するなら grabImage()、音声だけなら grabSamples() を使い、grab() を使うときは image と samples のどちらがnullでないかで分岐します。
音声フレームの中身は、samples の長さが1、要素が ShortBuffer で、残りは2,048個でした。FFmpegFrameGrabber の既定では、音声は16ビット整数でチャンネルを交互に並べた形で返るため、1,024サンプル×2チャンネルが1つのバッファに入っています。timestamp は21,333または21,334マイクロ秒ずつ進み、1,024÷48,000秒を整数マイクロ秒で表した値と整合しました。映像側の keyFrame は60枚中5枚で、OpenH264で書き出した前述の sample.mp4 では150枚中、先頭の1枚だけでした。前の章のシークの実測で、旧方式の setTimestamp がav.mp4で止まった0.8秒と1.2秒は、このキーフレームの位置(0.4秒ごと)と一致します。
FFmpegFrameRecorderの動画出力とLGPL版・GPL版の違い
Java 2Dで描いた画像を150フレーム並べて、5秒のMP4を書き出すコードです。コーデックは avcodec.AV_CODEC_ID_H264 のように整数のIDで指定します。
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Java2DFrameConverter;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
public class MakeVideo {
public static void main(String[] args) throws Exception {
int w = 640, h = 360, fps = 30, seconds = 5;
try (FFmpegFrameRecorder recorder = new FFmpegFrameRecorder("sample.mp4", w, h, 0);
Java2DFrameConverter conv = new Java2DFrameConverter()) {
recorder.setFormat("mp4");
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFrameRate(fps);
recorder.setVideoOption("crf", "23");
recorder.start();
for (int i = 0; i < fps * seconds; i++) {
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_3BYTE_BGR);
Graphics2D g = img.createGraphics();
g.setColor(Color.DARK_GRAY);
g.fillRect(0, 0, w, h);
g.setColor(Color.ORANGE);
g.fillOval(i * 3, 120, 120, 120);
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 40));
g.drawString("frame " + i, 20, 60);
g.dispose();
recorder.record(conv.convert(img));
}
recorder.stop();
}
}
}
このコードを標準の ffmpeg-platform で実行すると、FFmpegが次のログを出しました。
Info: avcodec_open2 rejected some options: Option: crf, value: 23
標準の ffmpeg-platform はLGPLの範囲でビルドされており、H.264のエンコーダーはlibx264ではなくCisco製のOpenH264です。OpenH264には crf というオプションが無いため、指定は無視され、ビットレートは既定の400kb/sになります。依存を ffmpeg-platform-gpl に差し替えると、libx264で書き出されます。
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform-gpl</artifactId>
<version>8.1.2-1.5.14</version>
</dependency>
同じコードを2つのビルドで実行し、出力をffprobeで比べた結果です。
| 項目 | ffmpeg-platform | ffmpeg-platform-gpl |
|---|---|---|
| H.264エンコーダー | libopenh264 | libx264 |
| プロファイル | Constrained Baseline | High |
crf 指定 |
無視される | 効く |
| ビットレート | 約351kb/s | 約55kb/s |
| ファイルサイズ | 220,727バイト | 36,907バイト |
GPL版はFFmpegのビルドに --enable-gpl、--enable-version3、--enable-libx264、--enable-libx265 を加えたものです。GPL版FFmpegとリンクして一体のプログラムとして配布する場合は、そのプログラム全体をGPLに沿って配布する必要があります(独立したプログラムを同じ媒体に収録するだけなら、この限りではありません)。アプリ全体をGPLに沿って配布できない場合はOpenH264を使い、LGPL版FFmpegの配布条件を満たしたうえで、画質はビットレートの指定(setVideoBitrate())で調整します。社内のサーバー処理のように配布しない用途なら、GPL版で問題ありません。
オプションが無視されたことは、上のように「rejected some options」というログでしか分かりません。1.5.12以降は FFmpegLogCallback.logRejectedOptions() も使えます。setVideoOption() を書いたら、ログに拒否の行が出ていないかを確認してください。
FrameからOpenCVのMatへの変換とCanny処理
OpenCVの関数に渡すには、OpenCVFrameConverter.ToMat で Frame を Mat に変換します。動画の2秒目のフレームをグレースケールにし、ガウシアンぼかしをかけてからCanny法でエッジを抽出する例です。
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.opencv_core.Mat;
import static org.bytedeco.opencv.global.opencv_core.countNonZero;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imwrite;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
public class CannyEdge {
public static void main(String[] args) throws Exception {
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber("sample.mp4");
OpenCVFrameConverter.ToMat toMat = new OpenCVFrameConverter.ToMat()) {
grabber.start();
grabber.setTimestamp(2_000_000L);
Frame frame = grabber.grabImage();
if (frame == null) {
throw new IllegalStateException("映像フレームを取得できませんでした");
}
Mat bgr = toMat.convert(frame);
Mat gray = new Mat();
Mat edges = new Mat();
cvtColor(bgr, gray, COLOR_BGR2GRAY);
GaussianBlur(gray, gray, new org.bytedeco.opencv.opencv_core.Size(5, 5), 0);
Canny(gray, edges, 50, 150);
imwrite("edges.png", edges);
System.out.println("channels=" + bgr.channels() + " edgePixels=" + countNonZero(edges));
gray.close();
edges.close();
grabber.stop();
}
}
}
実行結果は channels=3 edgePixels=1353 で、BGRの3チャンネルの画像からエッジ画像 edges.png が出力されました。OpenCVの関数名は org.bytedeco.opencv.global.opencv_imgproc などのクラスに静的メソッドとしてまとまっており、C++の cv::Canny とほぼ同じ引数で呼べます。Cannyの2つのしきい値の決め方は「エッジ検出とは?Canny法と微分フィルタの使い分け」で解説しています。
Mat はJavaのヒープの外にメモリを確保します。ガベージコレクションに任せると、ヒープに余裕があるうちは解放されず、プロセスのメモリが増え続けることがあります。ループの中で作る Mat は close() するか、JavaCPPの PointerScope を使ってまとめて解放します。
Webカメラの取り込みとCanvasFrameでの表示
Webカメラの映像は OpenCVFrameGrabber にデバイス番号(通常は0)を渡すか、FFmpegFrameGrabber にOSごとの入力形式(Windowsは dshow、macOSは avfoundation、Linuxは video4linux2)を指定して取り込みます。取り込んだ Frame を CanvasFrame の showImage() に渡すとウィンドウに表示され、同時に FFmpegFrameRecorder へ record() すれば録画になります。この節のカメラ処理は、手元の環境にカメラが無いため実行していません。
CanvasFrame はSwingのウィンドウなので、画面の無いサーバーやDockerコンテナ(java.awt.headless=true の環境)では使えません。サーバーで動かすときは表示を省き、画像ファイルかストリームに出力してください。
JavaCVの依存不足・JNI警告・オプション拒否の原因
UnsatisfiedLinkError: no jniavutilの原因と依存確認
OS別のネイティブJARがクラスパスに無いと、最初にFFmpegのクラスを使った時点で次のエラーになります。OpenCVなら jniopencv_core のように名前が変わります。
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class org.bytedeco.ffmpeg.global.avutil
Caused by: java.lang.ExceptionInInitializerError: Exception java.lang.UnsatisfiedLinkError: no jniavutil in java.library.path: ...
まず確認するのは次の3点です。依存を javacv だけにしている、-Djavacpp.platform= で指定したOSと実行するOSが違う(例:Macで linux-x86_64 を指定してビルドし、そのままMacで実行した)、fat JARを作るときにOS別JARが落ちている、の3つです。
Java 24以降で出るrestricted methodの警告
Java 25でJavaCVを動かすと、処理は成功しても標準エラーに次の警告が出ます。Java 26でも同じ警告が出ることを確認しました。
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.bytedeco.javacpp.Loader in an unnamed module (file:...javacpp-1.5.14.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
JDK 24で導入されたJEP 472(JNIの使用制限の準備)による警告で、JavaCPPがネイティブライブラリを読み込むときの System.load が対象です。警告にあるとおり、java の起動オプションに --enable-native-access=ALL-UNNAMED を付けると消えます。将来のJDKでは警告ではなくエラーになる予定なので、起動スクリプトとDockerfileには今のうちに入れておきます。Java 17や21で動かしている場合は警告自体が出ません。JVMの起動オプションについては「Java仮想マシン(JVM)とは?」も参照してください。
avformat_open_inputのオプション拒否ログと影響の判別
FFmpegFrameGrabber が入力を開く際、入力形式が受け付けない画素形式オプションを渡すと、Option: pixel_format, value: bgr24 が拒否されたというInfoログが出ることがあります。今回の例ではフレームは正しくBGRの3チャンネルで取れており、処理には影響しませんでした。一方、前の章の crf のように、意図した設定が効いていないことを示す場合もあります。どのオプションが拒否されたかを見て判断してください。
よくある質問
JavaCVは商用利用できますか?
できます。JavaCV本体はApache License 2.0とGPL(バージョン2以降、Classpath例外付き)から選べ、LICENSEには商用プロジェクトでも使えると書かれています。注意が必要なのは同梱するネイティブライブラリ側で、ffmpeg-platform-gpl を使うとGPLが適用されます。
javacvとjavacv-platformの違いは何ですか?
javacv はJavaのクラスだけで、ネイティブライブラリを含みません。javacv-platform はそれに加えてOS別のネイティブJARを引き込みます。アプリの依存には javacv-platform を書き、サイズは -Djavacpp.platform= で絞ります。
Apple Silicon(M1以降)のMacで使えますか?
1.5.14には macosx-arm64 用のネイティブJARが用意されています。javacpp.platform を指定しなければ自動で含まれ、指定する場合は macosx-arm64 にします。この記事のコードはIntel Mac(macosx-x86_64)で実行したもので、arm64では動かしていません。
OpenCV 5はJavaCVで使えますか?
2026年10月時点では使えません。最新のJavaCV 1.5.14が同梱するのはOpenCV 4.14.0です。OpenCV 5.0.0の公式成果物はAndroid向けAAR(org.opencv:opencv、最新は2026年6月26日公開の5.0.0.1)として出ていますが、デスクトップのJavaから使う場合、Windowsでは公式配布パッケージのJava用ファイルを利用でき、その他のOSではソースビルドが基本です。
JavaCVでOCRはできますか?
できます。javacv-platform にはTesseract 5.5.3のPresetsが含まれており、org.bytedeco.tesseract パッケージから呼び出せます。Tesseractの設定(PSM・OEM)や日本語の精度については「Tesseractとは?」で解説しています。