注: このページでは、Camera2 パッケージについて言及しています。アプリで Camera2 の特定の低レベル機能が必要な場合を除き、CameraX の使用をおすすめします。CameraX と Camera2 は、どちらも Android 5.0(API レベル 21)以降に対応しています。
カメラアプリは、複数のフレーム ストリームを同時に使用できます。場合によっては、ストリームごとに異なるフレーム解像度やピクセル形式が必要になることもあります。一般的なユースケースは次のとおりです。
- 動画撮影: 1 つのストリームはプレビュー用、もう 1 つはエンコードされてファイルに保存されます。
- バーコード スキャン: 1 つのストリームはプレビュー用、もう 1 つはバーコード検出用。
- 計算写真学: プレビュー用のストリームと、顔またはシーン検出用のストリーム。
フレームの処理には、無視できないパフォーマンス コストがかかります。このコストは、並列ストリーム処理やパイプライン処理を行うと、さらに増大します。
CPU、GPU、DSP などのリソースは、フレームワークの再処理機能を利用できる場合がありますが、メモリなどのリソースは直線的に増加します。
リクエストごとに複数のターゲット
複数のカメラ ストリームを 1 つの CameraCaptureRequest に結合できます。次のコード スニペットは、カメラ プレビュー用のストリームと画像処理用のストリームを使用してカメラ セッションを設定する方法を示しています。
Kotlin
val session: CameraCaptureSession = ... // from CameraCaptureSession.StateCallback
// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use TEMPLATE_RECORD
val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
val combinedRequest = session.device.createCaptureRequest(requestTemplate)
// Link the Surface targets with the combined request
combinedRequest.addTarget(previewSurface)
combinedRequest.addTarget(imReaderSurface)
// In this basic case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request
session.setRepeatingRequest(combinedRequest.build(), null, null)
Java
CameraCaptureSession session = …; // from CameraCaptureSession.StateCallback
// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use TEMPLATE_RECORD
CaptureRequest.Builder combinedRequest = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// Link the Surface targets with the combined request
combinedRequest.addTarget(previewSurface);
combinedRequest.addTarget(imReaderSurface);
// In this basic case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request
session.setRepeatingRequest(combinedRequest.build(), null, null);
ターゲット サーフェスを正しく構成すると、このコードは、StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) と StreamComfigurationMap.GetOutputStallDuration(int, Size) で決定された最小 FPS を満たすストリームのみを生成します。実際のパフォーマンスはデバイスによって異なりますが、Android では、出力タイプ、出力サイズ、ハードウェア レベルの 3 つの変数に応じて、特定の組み合わせをサポートするための保証が提供されます。
サポートされていない変数の組み合わせを使用すると、低いフレームレートで動作する可能性があります。動作しない場合は、失敗コールバックのいずれかがトリガーされます。createCaptureSession のドキュメントには、動作が保証されているものが記載されています。
出力タイプ
出力タイプは、フレームがエンコードされる形式を指します。可能な値は、PRIV、YUV、JPEG、RAW です。これらについては、createCaptureSession のドキュメントで説明されています。
アプリケーションの出力タイプを選択する際、互換性を最大化することが目標であれば、フレーム分析には ImageFormat.YUV_420_888 を、静止画像には ImageFormat.JPEG を使用します。プレビューと録画のシナリオでは、SurfaceView、TextureView、MediaRecorder、MediaCodec、RenderScript.Allocation を使用する可能性があります。このような場合は、画像形式を指定しないでください。互換性を確保するため、内部で使用される実際の形式に関係なく、ImageFormat.PRIVATE としてカウントされます。CameraCharacteristics を指定してデバイスでサポートされている形式をクエリするには、次のコードを使用します。
Kotlin
val characteristics: CameraCharacteristics = ...
val supportedFormats = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).outputFormats
Java
CameraCharacteristics characteristics = …;
int[] supportedFormats = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputFormats();
出力サイズ
利用可能なすべての出力サイズは StreamConfigurationMap.getOutputSizes() にリストされますが、互換性に関連するのは PREVIEW と MAXIMUM の 2 つのみです。サイズは上限として機能します。サイズ PREVIEW のものが機能する場合、PREVIEW より小さいサイズのものも機能します。MAXIMUM の場合も同様です。これらのサイズについては、CameraDevice のドキュメントをご覧ください。
使用可能な出力サイズは、選択した形式によって異なります。CameraCharacteristics と形式を指定すると、次のように、利用可能な出力サイズをクエリできます。
Kotlin
val characteristics: CameraCharacteristics = ...
val outputFormat: Int = ... // such as ImageFormat.JPEG
val sizes = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
.getOutputSizes(outputFormat)
Java
CameraCharacteristics characteristics = …;
int outputFormat = …; // such as ImageFormat.JPEG
Size[] sizes = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
.getOutputSizes(outputFormat);
カメラのプレビューと録画のユースケースでは、ターゲット クラスを使用して、サポートされているサイズを決定します。この形式は、カメラ フレームワーク自体によって処理されます。
Kotlin
val characteristics: CameraCharacteristics = ...
val targetClass: Class<T> = ... // such as SurfaceView::class.java
val sizes = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
.getOutputSizes(targetClass)
Java
CameraCharacteristics characteristics = …;
int outputFormat = …; // such as ImageFormat.JPEG
Size[] sizes = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
.getOutputSizes(outputFormat);
MAXIMUM サイズを取得するには、出力サイズを面積で並べ替えて、最も大きいものを返します。
Kotlin
fun<T> getMaximumOutputSize(
characteristics: CameraCharacteristics, targetClass: Class<T>, format: Int? = null):
Size {
val config = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
// If image format is provided, use it to determine supported sizes; or else use target class
val allSizes = if (format == null)
config.getOutputSizes(targetClass) else config.getOutputSizes(format)
return allSizes.maxBy { it.height * it.width }
}
Java
@RequiresApi(api = Build.VERSION_CODES.N)
<T> Size getMaximumOutputSize(CameraCharacteristics characteristics,
Class<T> targetClass,
Integer format) {
StreamConfigurationMap config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
// If image format is provided, use it to determine supported sizes; else use target class
Size[] allSizes;
if (format == null) {
allSizes = config.getOutputSizes(targetClass);
} else {
allSizes = config.getOutputSizes(format);
}
return Arrays.stream(allSizes).max(Comparator.comparing(s -> s.getHeight() * s.getWidth())).get();
}
PREVIEW は、デバイスの画面解像度に最適なサイズまたは 1080p(1920x1080)のうち、いずれか小さいほうのサイズを指します。アスペクト比が画面のアスペクト比と完全に一致しない場合があるため、ストリームを全画面モードで表示するには、レターボックス表示または切り抜きを適用する必要があります。適切なプレビュー サイズを取得するには、ディスプレイが回転する可能性があることを考慮しながら、利用可能な出力サイズとディスプレイ サイズを比較します。
次のコードは、サイズの比較を少し簡単にするヘルパークラス SmartSize を定義しています。
Kotlin
/** Helper class used to pre-compute shortest and longest sides of a [Size] */
class SmartSize(width: Int, height: Int) {
var size = Size(width, height)
var long = max(size.width, size.height)
var short = min(size.width, size.height)
override fun toString() = "SmartSize(${long}x${short})"
}
/** Standard High Definition size for pictures and video */
val SIZE_1080P: SmartSize = SmartSize(1920, 1080)
/** Returns a [SmartSize] object for the given [Display] */
fun getDisplaySmartSize(display: Display): SmartSize {
val outPoint = Point()
display.getRealSize(outPoint)
return SmartSize(outPoint.x, outPoint.y)
}
/**
* Returns the largest available PREVIEW size. For more information, see:
* https://d.android.com/reference/android/hardware/camera2/CameraDevice
*/
fun<T> getPreviewOutputSize(
display: Display,
characteristics: CameraCharacteristics,
targetClass: Class<T>,
format: Int? = null
): Size {
// Find which is smaller: screen or 1080p
val screenSize = getDisplaySmartSize(display)
val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short
val maxSize = if (hdScreen) SIZE_1080P else screenSize
// If image format is provided, use it to determine supported sizes; else use target class
val config = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
if (format == null)
assert(StreamConfigurationMap.isOutputSupportedFor(targetClass))
else
assert(config.isOutputSupportedFor(format))
val allSizes = if (format == null)
config.getOutputSizes(targetClass) else config.getOutputSizes(format)
// Get available sizes and sort them by area from largest to smallest
val validSizes = allSizes
.sortedWith(compareBy { it.height * it.width })
.map { SmartSize(it.width, it.height) }.reversed()
// Then, get the largest output size that is smaller or equal than our max size
return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size
}
Java
/** Helper class used to pre-compute shortest and longest sides of a [Size] */
class SmartSize {
Size size;
double longSize;
double shortSize;
public SmartSize(Integer width, Integer height) {
size = new Size(width, height);
longSize = max(size.getWidth(), size.getHeight());
shortSize = min(size.getWidth(), size.getHeight());
}
@Override
public String toString() {
return String.format("SmartSize(%sx%s)", longSize, shortSize);
}
}
/** Standard High Definition size for pictures and video */
SmartSize SIZE_1080P = new SmartSize(1920, 1080);
/** Returns a [SmartSize] object for the given [Display] */
SmartSize getDisplaySmartSize(Display display) {
Point outPoint = new Point();
display.getRealSize(outPoint);
return new SmartSize(outPoint.x, outPoint.y);
}
/**
* Returns the largest available PREVIEW size. For more information, see:
* https://d.android.com/reference/android/hardware/camera2/CameraDevice
*/
@RequiresApi(api = Build.VERSION_CODES.N)
<T> Size getPreviewOutputSize(
Display display,
CameraCharacteristics characteristics,
Class<T> targetClass,
Integer format
){
// Find which is smaller: screen or 1080p
SmartSize screenSize = getDisplaySmartSize(display);
boolean hdScreen = screenSize.longSize >= SIZE_1080P.longSize || screenSize.shortSize >= SIZE_1080P.shortSize;
SmartSize maxSize;
if (hdScreen) {
maxSize = SIZE_1080P;
} else {
maxSize = screenSize;
}
// If image format is provided, use it to determine supported sizes; else use target class
StreamConfigurationMap config = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (format == null)
assert(StreamConfigurationMap.isOutputSupportedFor(targetClass));
else
assert(config.isOutputSupportedFor(format));
Size[] allSizes;
if (format == null) {
allSizes = config.getOutputSizes(targetClass);
} else {
allSizes = config.getOutputSizes(format);
}
// Get available sizes and sort them by area from largest to smallest
List<Size> sortedSizes = Arrays.asList(allSizes);
List<SmartSize> validSizes =
sortedSizes.stream()
.sorted(Comparator.comparing(s -> s.getHeight() * s.getWidth()))
.map(s -> new SmartSize(s.getWidth(), s.getHeight()))
.sorted(Collections.reverseOrder()).collect(Collectors.toList());
// Then, get the largest output size that is smaller or equal than our max size
return validSizes.stream()
.filter(s -> s.longSize <= maxSize.longSize && s.shortSize <= maxSize.shortSize)
.findFirst().get().size;
}
サポートされているハードウェア レベルを確認する
実行時に利用可能な機能を判断するには、CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL を使用して、サポートされているハードウェア レベルを確認します。
CameraCharacteristics オブジェクトを使用すると、1 つのステートメントでハードウェア レベルを取得できます。
Kotlin
val characteristics: CameraCharacteristics = ...
// Hardware level will be one of:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
val hardwareLevel = characteristics.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
Java
CameraCharacteristics characteristics = ...;
// Hardware level will be one of:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
Integer hardwareLevel = characteristics.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
すべての要素を組み合わせる
出力タイプ、出力サイズ、ハードウェア レベルを使用して、有効なストリームの組み合わせを決定できます。次の表は、LEGACY ハードウェア レベルの CameraDevice でサポートされている構成のスナップショットです。
| 目標 1 | 目標 2 | 目標 3 | ユースケースの例 | |||
|---|---|---|---|---|---|---|
| タイプ | 最大サイズ | タイプ | 最大サイズ | タイプ | 最大サイズ | |
PRIV |
MAXIMUM |
シンプルなプレビュー、GPU 動画処理、プレビューなしの動画撮影。 | ||||
JPEG |
MAXIMUM |
ビューファインダーなしの静止画撮影。 | ||||
YUV |
MAXIMUM |
アプリ内での動画 / 画像処理。 | ||||
PRIV |
PREVIEW |
JPEG |
MAXIMUM |
標準の静止画撮影。 | ||
YUV |
PREVIEW |
JPEG |
MAXIMUM |
アプリ内処理と静止画撮影。 | ||
PRIV |
PREVIEW |
PRIV |
PREVIEW |
標準録画。 | ||
PRIV |
PREVIEW |
YUV |
PREVIEW |
プレビューとアプリ内処理。 | ||
PRIV |
PREVIEW |
YUV |
PREVIEW |
JPEG |
MAXIMUM |
静止画キャプチャとアプリ内処理。 |
LEGACY は、可能な限り低いハードウェア レベルです。この表は、Camera2(API レベル 21 以降)をサポートするすべてのデバイスが、適切な構成を使用し、メモリ、CPU、熱の制約など、パフォーマンスを制限するオーバーヘッドが大きすぎない場合、最大 3 つの同時ストリームを出力できることを示しています。
アプリは、ターゲット出力バッファも構成する必要があります。たとえば、LEGACY ハードウェア レベルのデバイスをターゲットにするには、2 つのターゲット出力サーフェスを設定します。1 つは ImageFormat.PRIVATE を使用し、もう 1 つは ImageFormat.YUV_420_888 を使用します。これは、PREVIEW サイズを使用する場合にサポートされる組み合わせです。このドキュメントで前述した関数を使用して、カメラ ID に必要なプレビュー サイズを取得するには、次のコードが必要です。
Kotlin
val characteristics: CameraCharacteristics = ...
val context = this as Context // assuming you are inside of an activity
val surfaceViewSize = getPreviewOutputSize(
context, characteristics, SurfaceView::class.java)
val imageReaderSize = getPreviewOutputSize(
context, characteristics, ImageReader::class.java, format = ImageFormat.YUV_420_888)
Java
CameraCharacteristics characteristics = ...;
Context context = this; // assuming you are inside of an activity
Size surfaceViewSize = getPreviewOutputSize(
context, characteristics, SurfaceView.class);
Size imageReaderSize = getPreviewOutputSize(
context, characteristics, ImageReader.class, format = ImageFormat.YUV_420_888);
提供されたコールバックを使用して、SurfaceView の準備が整うまで待機する必要があります。
Kotlin
val surfaceView = findViewById<SurfaceView>(...)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
// You do not need to specify image format, and it will be considered of type PRIV
// Surface is now ready and you could use it as an output target for CameraSession
}
...
})
Java
SurfaceView surfaceView = findViewById<SurfaceView>(...);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
// You do not need to specify image format, and it will be considered of type PRIV
// Surface is now ready and you could use it as an output target for CameraSession
}
...
});
SurfaceHolder.setFixedSize() を呼び出すことで、SurfaceView をカメラの出力サイズに強制的に一致させることができます。または、GitHub のカメラサンプルの共通モジュールの AutoFitSurfaceView と同様のアプローチを取ることもできます。このアプローチでは、アスペクト比と利用可能なスペースの両方を考慮して絶対サイズを設定し、アクティビティの変更がトリガーされたときに自動調整します。
選択したフォーマットで ImageReader からもう一方のサーフェスをセットアップする方が簡単です。待機するコールバックがないためです。
Kotlin
val frameBufferCount = 3 // just an example, depends on your usage of ImageReader
val imageReader = ImageReader.newInstance(
imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
frameBufferCount)
Java
int frameBufferCount = 3; // just an example, depends on your usage of ImageReader
ImageReader imageReader = ImageReader.newInstance(
imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
frameBufferCount);
ImageReader などのブロッキング ターゲット バッファを使用する場合は、フレームを使用後に破棄します。
Kotlin
imageReader.setOnImageAvailableListener({
val frame = it.acquireNextImage()
// Do something with "frame" here
it.close()
}, null)
Java
imageReader.setOnImageAvailableListener(listener -> {
Image frame = listener.acquireNextImage();
// Do something with "frame" here
listener.close();
}, null);
LEGACY ハードウェア レベルは、最大公約数的なデバイスを対象としています。条件分岐を追加して、LIMITED ハードウェア レベルのデバイスで出力ターゲット サーフェスの 1 つに RECORD サイズを使用したり、FULL ハードウェア レベルのデバイスで MAXIMUM サイズに増やしたりすることもできます。