blob: fc6d05e34a703c882eca80af9d0527d2c2fb669a [file] [log] [blame]
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.camera.core.imagecapture;
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
import static androidx.core.util.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import android.media.ImageReader;
import android.os.Build;
import android.util.Size;
import android.view.Surface;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ForwardingImageProxy;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.MetadataImageReader;
import androidx.camera.core.SafeCloseImageReaderProxy;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImageReaderProxy;
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.processing.Edge;
import androidx.camera.core.processing.Node;
import com.google.auto.value.AutoValue;
import java.util.HashSet;
import java.util.Set;
/**
* A {@link Node} that calls back when all the images for one capture are received.
*
* <p>This {@link Node} waits for all the images in a {@link ProcessingRequest} are received
* before invoking the {@link ProcessingRequest#onImageCaptured()} callback. Then it makes
* sure that the {@link ImageProxy} are omitted after their corresponding {@link ProcessingRequest}.
*
* <p>It's also responsible for managing the {@link ImageReaderProxy}. It makes sure that the
* queue is not overflowed.
*
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
class CaptureNode implements Node<CaptureNode.In, CaptureNode.Out> {
// TODO: we might need to calculate this number dynamically based on on many frames are
// needed by the post-processing. e.g. night mode might need to merge 10+ frames. 4 images
// should be enough for now.
private static final int MAX_IMAGES = 4;
@NonNull
private final Set<Integer> mPendingStageIds = new HashSet<>();
private final Set<ImageProxy> mPendingImages = new HashSet<>();
private ProcessingRequest mCurrentRequest = null;
SafeCloseImageReaderProxy mSafeCloseImageReaderProxy;
private Out mOutputEdge;
private In mInputEdge;
@NonNull
@Override
public Out transform(@NonNull In inputEdge) {
mInputEdge = inputEdge;
Size size = inputEdge.getSize();
int format = inputEdge.getFormat();
// Creates ImageReaders.
MetadataImageReader metadataImageReader = new MetadataImageReader(size.getWidth(),
size.getHeight(), format, MAX_IMAGES);
mSafeCloseImageReaderProxy = new SafeCloseImageReaderProxy(metadataImageReader);
inputEdge.setCameraCaptureCallback(metadataImageReader.getCameraCaptureCallback());
inputEdge.setSurface(requireNonNull(metadataImageReader.getSurface()));
// Listen to the input edges.
metadataImageReader.setOnImageAvailableListener(imageReader -> onImageProxyAvailable(
requireNonNull(imageReader.acquireNextImage())), mainThreadExecutor());
inputEdge.getRequestEdge().setListener(this::onRequestAvailable);
mOutputEdge = Out.of(inputEdge.getFormat());
return mOutputEdge;
}
@VisibleForTesting
@MainThread
void onImageProxyAvailable(@NonNull ImageProxy imageProxy) {
checkMainThread();
if (mCurrentRequest == null) {
// Request has not arrived yet. Track the image and match later.
mPendingImages.add(imageProxy);
} else {
// Match image and send it downstream.
matchAndPropagateImage(imageProxy);
}
}
private void matchAndPropagateImage(@NonNull ImageProxy imageProxy) {
// Check if the capture is complete.
int stageId = (Integer) requireNonNull(
imageProxy.getImageInfo().getTagBundle().getTag(mCurrentRequest.getTagBundleKey()));
checkState(mPendingStageIds.contains(stageId),
"Received an unexpected stage id" + stageId);
mPendingStageIds.remove(stageId);
if (mPendingStageIds.isEmpty()) {
// The capture is complete. Let the pipeline know it can take another picture.
mCurrentRequest.onImageCaptured();
mCurrentRequest = null;
}
// Send the image downstream.
mOutputEdge.getImageEdge().accept(imageProxy);
}
@VisibleForTesting
@MainThread
void onRequestAvailable(@NonNull ProcessingRequest request) {
checkMainThread();
// Unable to issue request if the queue has no capacity.
checkState(getCapacity() > 0,
"Too many acquire images. Close image to be able to process next.");
// Check if there is already a current request. Only one concurrent request is allowed.
checkState(mCurrentRequest == null || mPendingStageIds.isEmpty(),
"The previous request is not complete");
// Track the request and its stage IDs.
mCurrentRequest = request;
mPendingStageIds.addAll(request.getStageIds());
// Send the request downstream.
mOutputEdge.getRequestEdge().accept(request);
// Match pending images and send them downstream.
for (ImageProxy imageProxy : mPendingImages) {
matchAndPropagateImage(imageProxy);
}
mPendingImages.clear();
}
@MainThread
@Override
public void release() {
checkMainThread();
if (mSafeCloseImageReaderProxy != null) {
mSafeCloseImageReaderProxy.safeClose();
}
if (mInputEdge != null) {
mInputEdge.closeSurface();
}
}
@VisibleForTesting
@NonNull
In getInputEdge() {
return mInputEdge;
}
@MainThread
public int getCapacity() {
checkMainThread();
checkState(mSafeCloseImageReaderProxy != null,
"The ImageReader is not initialized.");
return mSafeCloseImageReaderProxy.getCapacity();
}
@MainThread
public void setOnImageCloseListener(ForwardingImageProxy.OnImageCloseListener listener) {
checkMainThread();
checkState(mSafeCloseImageReaderProxy != null,
"The ImageReader is not initialized.");
mSafeCloseImageReaderProxy.setOnImageCloseListener(listener);
}
/**
* Input edges of a {@link CaptureNode}.
*/
@AutoValue
abstract static class In {
private CameraCaptureCallback mCameraCaptureCallback;
private DeferrableSurface mSurface;
/**
* Size of the {@link ImageReader} buffer.
*/
abstract Size getSize();
/**
* Size of the {@link ImageReader} format.
*/
abstract int getFormat();
/**
* Edge that accepts {@link ProcessingRequest}.
*/
@NonNull
abstract Edge<ProcessingRequest> getRequestEdge();
/**
* Edge that accepts the image frames.
*
* <p>The value will be used in a capture request sent to the camera.
*/
@NonNull
DeferrableSurface getSurface() {
return mSurface;
}
void setSurface(@NonNull Surface surface) {
checkState(mSurface == null, "The surface is already set.");
mSurface = new ImmediateSurface(surface);
}
void closeSurface() {
mSurface.close();
}
/**
* Edge that accepts image metadata.
*
* <p>The value will be used in a capture request sent to the camera.
*/
CameraCaptureCallback getCameraCaptureCallback() {
return mCameraCaptureCallback;
}
void setCameraCaptureCallback(@NonNull CameraCaptureCallback cameraCaptureCallback) {
mCameraCaptureCallback = cameraCaptureCallback;
}
@NonNull
static In of(Size size, int format) {
return new AutoValue_CaptureNode_In(size, format, new Edge<>());
}
}
/**
* Output edges of a {@link CaptureNode}.
*/
@AutoValue
abstract static class Out {
/**
* Edge that omits {@link ImageProxy}s.
*
* <p>The frames will be closed by downstream nodes.
*/
abstract Edge<ImageProxy> getImageEdge();
/**
* Edge that omits {@link ProcessingRequest}.
*/
abstract Edge<ProcessingRequest> getRequestEdge();
/**
* Format of the {@link ImageProxy} in {@link #getImageEdge()}.
*/
abstract int getFormat();
static Out of(int format) {
return new AutoValue_CaptureNode_Out(new Edge<>(), new Edge<>(), format);
}
}
}