1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.camera.core;
18 
19 import android.util.Rational;
20 import android.view.Surface;
21 import android.view.SurfaceView;
22 import android.view.View;
23 
24 import androidx.annotation.IntDef;
25 import androidx.annotation.RestrictTo;
26 import androidx.camera.core.impl.ImageOutputConfig;
27 import androidx.camera.core.resolutionselector.AspectRatioStrategy;
28 import androidx.camera.core.resolutionselector.ResolutionSelector;
29 import androidx.core.util.Preconditions;
30 
31 import org.jspecify.annotations.NonNull;
32 
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.util.concurrent.Executor;
36 
37 /**
38  * The field of view of one or many {@link UseCase}s.
39  *
40  * <p> The {@link ViewPort} defines a FOV which is used by CameraX to calculate output crop rects.
41  * For use cases associated with the same {@link ViewPort} in a {@link UseCaseGroup}, the output
42  * crop rect will be mapped to the same camera sensor area. Usually {@link ViewPort} is
43  * configured to optimize for {@link Preview} so that {@link ImageAnalysis} and
44  * {@link ImageCapture} produce the same crop rect in a WYSIWYG way.
45  *
46  * <p> If the {@link ViewPort} is used with a {@link ImageCapture} and
47  * {@link ImageCapture#takePicture(
48  *ImageCapture.OutputFileOptions, Executor, ImageCapture.OnImageSavedCallback)} is called,
49  * the image may be cropped before saving to disk which introduces an additional
50  * latency. To avoid the latency and get the uncropped image, please use the in-memory method
51  * {@link ImageCapture#takePicture(Executor, ImageCapture.OnImageCapturedCallback)}.
52  *
53  * <p> For {@link ImageAnalysis} and in-memory {@link ImageCapture}, the output crop rect is
54  * {@link ImageProxy#getCropRect()}; for on-disk {@link ImageCapture}, the image is cropped before
55  * saving; for {@link Preview}, the crop rect is
56  * {@link SurfaceRequest.TransformationInfo#getCropRect()}. Caller should transform the output in
57  * a way that only the area defined by the crop rect is visible to end users. Once the crop rect
58  * is applied, all the use cases will produce the same image with possibly different resolutions.
59  */
60 public final class ViewPort {
61 
62     /**
63      * LayoutDirection that defines the start and end of the {@link ScaleType}.
64      *
65      * @see android.util.LayoutDirection
66      */
67     @IntDef({android.util.LayoutDirection.LTR, android.util.LayoutDirection.RTL})
68     @Retention(RetentionPolicy.SOURCE)
69     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
70     public @interface LayoutDirection {
71     }
72 
73     /**
74      * Scale types used to calculate the crop rect for a {@link UseCase}.
75      *
76      */
77     @IntDef({FILL_START, FILL_CENTER, FILL_END, FIT})
78     @Retention(RetentionPolicy.SOURCE)
79     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
80     public @interface ScaleType {
81     }
82 
83     /**
84      * Generate a crop rect that once applied, it scales the output while maintaining its aspect
85      * ratio, so it fills the entire {@link ViewPort}, and align it to the start of the
86      * {@link ViewPort}, which is the top left corner in a left-to-right (LTR) layout, or the top
87      * right corner in a right-to-left (RTL) layout.
88      * <p>
89      * This may cause the output to be cropped if the output aspect ratio does not match that of
90      * the {@link ViewPort}.
91      */
92     public static final int FILL_START = 0;
93 
94     /**
95      * Generate a crop rect that once applied, it scales the output while maintaining its aspect
96      * ratio, so it fills the entire {@link ViewPort} and center it.
97      * <p>
98      * This may cause the output to be cropped if the output aspect ratio does not match that of
99      * the {@link ViewPort}.
100      */
101     public static final int FILL_CENTER = 1;
102 
103     /**
104      * Generate a crop rect that once applied, it scales the output while maintaining its aspect
105      * ratio, so it fills the entire {@link ViewPort}, and align it to the end of the
106      * {@link ViewPort}, which is the bottom right corner in a left-to-right (LTR) layout, or the
107      * bottom left corner in a right-to-left (RTL) layout.
108      * <p>
109      * This may cause the output to be cropped if the output aspect ratio does not match that of
110      * the {@link ViewPort}.
111      */
112     public static final int FILL_END = 2;
113 
114     /**
115      * Generate the max possible crop rect ignoring the aspect ratio. For {@link ImageAnalysis}
116      * and {@link ImageCapture}, the output will be an image defined by the crop rect.
117      *
118      * <p> For {@link Preview}, further calculation is needed to to fit the crop rect into the
119      * viewfinder. Code sample below is a simplified version assuming {@link Surface}
120      * orientation is the same as the camera sensor orientation, the viewfinder is a
121      * {@link SurfaceView} and the viewfinder's pixel width/height is the same as the size
122      * request by CameraX in {@link SurfaceRequest#getResolution()}. For more complicated
123      * scenarios, please check out the source code of PreviewView in androidx.camera.view artifact.
124      *
125      * <p> First, calculate the transformation to fit the crop rect in the center of the viewfinder:
126      *
127      * <pre>{@code
128      *   val transformation = Matrix()
129      *   transformation.setRectToRect(
130      *       cropRect, new RectF(0, 0, viewFinder.width, viewFinder.height, ScaleToFit.CENTER))
131      * }</pre>
132      *
133      * <p> Then apply the transformation to the viewfinder:
134      *
135      * <pre>{@code
136      *   val transformedRect = RectF(0, 0, viewFinder.width, viewFinder.height)
137      *   transformation.mapRect(surfaceRect)
138      *   viewFinder.pivotX = 0
139      *   viewFinder.pivotY = 0
140      *   viewFinder.translationX = transformedRect.left
141      *   viewFinder.translationY = transformedRect.top
142      *   viewFinder.scaleX = surfaceRect.width/transformedRect.width
143      *   viewFinder.scaleY = surfaceRect.height/transformedRect.height
144      * }</pre>
145      */
146     public static final int FIT = 3;
147 
148     @ScaleType
149     private int mScaleType;
150 
151     private @NonNull Rational mAspectRatio;
152 
153     @ImageOutputConfig.RotationValue
154     private int mRotation;
155 
156     @LayoutDirection
157     private int mLayoutDirection;
158 
ViewPort(@caleType int scaleType, @NonNull Rational aspectRatio, @ImageOutputConfig.RotationValue int rotation, @LayoutDirection int layoutDirection)159     ViewPort(@ScaleType int scaleType, @NonNull Rational aspectRatio,
160             @ImageOutputConfig.RotationValue int rotation, @LayoutDirection int layoutDirection) {
161         mScaleType = scaleType;
162         mAspectRatio = aspectRatio;
163         mRotation = rotation;
164         mLayoutDirection = layoutDirection;
165     }
166 
167     /**
168      * Gets the aspect ratio of the {@link ViewPort}.
169      */
getAspectRatio()170     public @NonNull Rational getAspectRatio() {
171         return mAspectRatio;
172     }
173 
174     /**
175      * Gets the rotation of the {@link ViewPort}.
176      */
177     @ImageOutputConfig.RotationValue
getRotation()178     public int getRotation() {
179         return mRotation;
180     }
181 
182     /**
183      * Gets the scale type of the {@link ViewPort}.
184      */
185     @ScaleType
getScaleType()186     public int getScaleType() {
187         return mScaleType;
188     }
189 
190     /**
191      * Gets the layout direction of the {@link ViewPort}.
192      */
193     @LayoutDirection
getLayoutDirection()194     public int getLayoutDirection() {
195         return mLayoutDirection;
196     }
197 
198     /**
199      * Builder for {@link ViewPort}.
200      */
201     public static final class Builder {
202 
203         private static final int DEFAULT_LAYOUT_DIRECTION = android.util.LayoutDirection.LTR;
204         @ScaleType
205         private static final int DEFAULT_SCALE_TYPE = FILL_CENTER;
206 
207         @ScaleType
208         private int mScaleType = DEFAULT_SCALE_TYPE;
209 
210         private final Rational mAspectRatio;
211 
212         @ImageOutputConfig.RotationValue
213         private final int mRotation;
214 
215         @LayoutDirection
216         private int mLayoutDirection = DEFAULT_LAYOUT_DIRECTION;
217 
218         /**
219          * Creates {@link ViewPort.Builder} with aspect ratio and rotation.
220          *
221          * <p> To create a {@link ViewPort} that is based on the {@link Preview} use
222          * case, the aspect ratio should be the dimension of the {@link View} and
223          * the rotation should be the value of {@link Preview#getTargetRotation()}:
224          *
225          * <pre>{@code
226          * val aspectRatio = Rational(viewFinder.width, viewFinder.height)
227          * val viewport = ViewPort.Builder(aspectRatio, preview.getTargetRotation()).build()
228          * }</pre>
229          *
230          * <p> In a scenario where {@link Preview} is not used, for example, face detection in
231          * {@link ImageAnalysis} and taking pictures with {@link ImageCapture} when faces are
232          * found, the {@link ViewPort} should be created with the aspect ratio and rotation of the
233          * {@link ImageCapture} use case.
234          *
235          * <p>All {@link UseCase}s have a configurable aspect ratio setting that determines the
236          * supported sizes for creating the capture session to receive images from the camera.
237          * See {@link ResolutionSelector.Builder#setAspectRatioStrategy(AspectRatioStrategy)} for
238          * {@link Preview}, {@link ImageCapture}, and {@link ImageAnalysis}, and
239          * {@link androidx.camera.video.Recorder.Builder#setAspectRatio(int)} for
240          * {@link androidx.camera.video.VideoCapture}. This is distinct from the {@link ViewPort}
241          * 's aspect ratio setting, which is used to calculate output crop rectangles among bound
242          * {@link UseCase}s to ensure that they have the same content.
243          *
244          * <p>To obtain the maximum field of view (FOV) of the full camera sensor, it is
245          * recommended that the {@link ViewPort} and the bound {@link UseCase}s have matching
246          * aspect ratio settings. Otherwise, the output crop rectangles may be a double-cropped
247          * result from the full camera sensor FOV.
248          *
249          * <p>For example, if all {@link UseCase}s have a 16:9 aspect ratio preference setting
250          * and are bound with a {@link ViewPort} with a 4:3 aspect ratio, the images obtained
251          * from the camera will be 16:9 images cropped from the 4:3 camera sensor data. The
252          * content inside the output crop rectangles will be 4:3 cropped images from the 16:9
253          * images cropped from the 4:3 camera sensor data. In other words, the images will be
254          * cropped twice: first to fit the 16:9 aspect ratio, and then to fit the 4:3 aspect
255          * ratio. This will result in a loss of FOV. To avoid this, it is important to ensure
256          * that the {@link ViewPort} and the bound {@link UseCase}s have matching aspect ratio
257          * settings.
258          *
259          * @param aspectRatio aspect ratio of the output crop rect if the scale type
260          *                    is FILL_START, FILL_CENTER or FILL_END. This is usually the
261          *                    width/height of the preview viewfinder that displays the camera
262          *                    feed. The value is ignored if the scale type is FIT.
263          * @param rotation    The rotation value is one of four valid values:
264          *                    {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
265          *                    {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
266          */
Builder(@onNull Rational aspectRatio, @ImageOutputConfig.RotationValue int rotation)267         public Builder(@NonNull Rational aspectRatio,
268                 @ImageOutputConfig.RotationValue int rotation) {
269             mAspectRatio = aspectRatio;
270             mRotation = rotation;
271         }
272 
273         /**
274          * Sets the scale type of the {@link ViewPort}.
275          *
276          * <p> The value is used by {@link UseCase} to calculate the crop rect.
277          *
278          * <p> The default value is {@link #FILL_CENTER} if not set.
279          */
setScaleType(@caleType int scaleType)280         public @NonNull Builder setScaleType(@ScaleType int scaleType) {
281             mScaleType = scaleType;
282             return this;
283         }
284 
285         /**
286          * Sets the layout direction of the {@link ViewPort}.
287          *
288          * <p> The layout direction decides the start and the end of the crop rect if
289          * the scale type is {@link #FILL_END} or {@link #FILL_START}.
290          *
291          * <p> The default value is {@link android.util.LayoutDirection#LTR} if not set.
292          */
setLayoutDirection(@ayoutDirection int layoutDirection)293         public @NonNull Builder setLayoutDirection(@LayoutDirection int layoutDirection) {
294             mLayoutDirection = layoutDirection;
295             return this;
296         }
297 
298         /**
299          * Builds the {@link ViewPort}.
300          */
build()301         public @NonNull ViewPort build() {
302             Preconditions.checkNotNull(mAspectRatio, "The crop aspect ratio must be set.");
303             return new ViewPort(mScaleType, mAspectRatio, mRotation, mLayoutDirection);
304         }
305     }
306 }
307