1# Copyright 2024 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Verify session characteristics zoom."""
15
16import logging
17import os
18
19from mobly import test_runner
20import numpy as np
21
22import its_base_test
23import camera_properties_utils
24import capture_request_utils
25import image_processing_utils
26import its_session_utils
27import zoom_capture_utils
28
29_CIRCLISH_RTOL = 0.065  # contour area vs ideal circle area pi*((w+h)/4)**2
30_FPS_30_60 = (30, 60)
31_FPS_SELECTION_ATOL = 0.01
32_FPS_ATOL = 0.8
33_MAX_FPS_INDEX = 1
34_MAX_STREAM_COUNT = 2
35_NAME = os.path.splitext(os.path.basename(__file__))[0]
36_SEC_TO_NSEC = 1_000_000_000
37
38
39class SessionCharacteristicsZoomTest(its_base_test.ItsBaseTest):
40  """Tests camera capture session specific zoom behavior.
41
42  The combination of camera features tested by this function are:
43  - Preview stabilization
44  - Target FPS range
45  - HLG 10-bit HDR
46  """
47
48  def test_session_characteristics_zoom(self):
49    with its_session_utils.ItsSession(
50        device_id=self.dut.serial,
51        camera_id=self.camera_id) as cam:
52
53      # Skip if the device doesn't support feature combination query
54      props = cam.get_camera_properties()
55      feature_combination_query_version = props.get(
56          'android.info.sessionConfigurationQueryVersion')
57      if not feature_combination_query_version:
58        feature_combination_query_version = (
59            its_session_utils.ANDROID14_API_LEVEL
60        )
61      camera_properties_utils.skip_unless(
62          feature_combination_query_version >=
63          its_session_utils.ANDROID15_API_LEVEL)
64
65      # Raise error if not FRONT or REAR facing camera
66      camera_properties_utils.check_front_or_rear_camera(props)
67
68      # Load chart for scene
69      its_session_utils.load_scene(
70          cam, props, self.scene, self.tablet, self.chart_distance)
71
72      # set TOLs based on camera and test rig params
73      debug = self.debug_mode
74      if camera_properties_utils.logical_multi_camera(props):
75        test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size(
76            cam, props, self.chart_distance, debug)
77      else:
78        test_tols = {}
79        fls = props['android.lens.info.availableFocalLengths']
80        for fl in fls:
81          test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL,
82                           zoom_capture_utils.OFFSET_RTOL)
83        yuv_size = capture_request_utils.get_largest_yuv_format(props)
84        size = [yuv_size['width'], yuv_size['height']]
85      logging.debug('capture size: %s', size)
86      logging.debug('test TOLs: %s', test_tols)
87
88      # List of queryable stream combinations
89      combinations_str, combinations = cam.get_queryable_stream_combinations()
90      logging.debug('Queryable stream combinations: %s', combinations_str)
91
92      # Stabilization modes. Make sure to test ON first.
93      stabilization_params = []
94      stabilization_modes = props[
95          'android.control.availableVideoStabilizationModes']
96      if (camera_properties_utils.STABILIZATION_MODE_PREVIEW in
97          stabilization_modes):
98        stabilization_params.append(
99            camera_properties_utils.STABILIZATION_MODE_PREVIEW)
100      stabilization_params.append(
101          camera_properties_utils.STABILIZATION_MODE_OFF)
102      logging.debug('stabilization modes: %s', stabilization_params)
103
104      configs = props['android.scaler.streamConfigurationMap'][
105          'availableStreamConfigurations']
106      fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props)
107
108      test_failures = []
109      for stream_combination in combinations:
110        streams_name = stream_combination['name']
111        min_frame_duration = 0
112        configured_streams = []
113        skip = False
114
115        # Only supports combinations of up to 2 streams
116        if len(stream_combination['combination']) > _MAX_STREAM_COUNT:
117          raise AssertionError(
118              f'stream combination cannot exceed {_MAX_STREAM_COUNT} streams.')
119
120        # Skip if combinations contains only 1 stream, which is preview
121        if len(stream_combination['combination']) == 1:
122          continue
123
124        for i, stream in enumerate(stream_combination['combination']):
125          fmt = None
126          size = [int(e) for e in stream['size'].split('x')]
127          if stream['format'] == its_session_utils.PRIVATE_FORMAT:
128            fmt = capture_request_utils.FMT_CODE_PRIV
129          elif stream['format'] == 'jpeg':
130            fmt = capture_request_utils.FMT_CODE_JPEG
131          elif stream['format'] == its_session_utils.JPEG_R_FMT_STR:
132            fmt = capture_request_utils.FMT_CODE_JPEG_R
133          elif stream['format'] == 'yuv':
134            fmt = capture_request_utils.FMT_CODE_YUV
135
136          # Assume first stream is always a preview stream with priv format
137          if i == 0 and fmt != capture_request_utils.FMT_CODE_PRIV:
138            raise AssertionError(
139                'first stream in the combination must be priv format preview.')
140
141          # Second stream must be jpeg or yuv for zoom test. If not, skip
142          if (i == 1 and fmt != capture_request_utils.FMT_CODE_JPEG and
143              fmt != capture_request_utils.FMT_CODE_JPEG_R and
144              fmt != capture_request_utils.FMT_CODE_YUV):
145            logging.debug(
146                'second stream format %s is not yuv/jpeg/jpeg_r. Skip',
147                stream['format'])
148            skip = True
149            break
150
151          # Skip if size and format are not supported by the device.
152          config = [x for x in configs if
153                    x['format'] == fmt and
154                    x['width'] == size[0] and
155                    x['height'] == size[1]]
156          if not config:
157            logging.debug(
158                'stream combination %s not supported. Skip', streams_name)
159            skip = True
160            break
161
162          min_frame_duration = max(
163              config[0]['minFrameDuration'], min_frame_duration)
164          logging.debug(
165              'format is %s, min_frame_duration is %d}',
166              stream['format'], config[0]['minFrameDuration'])
167          configured_streams.append(
168              {'format': stream['format'], 'width': size[0], 'height': size[1]})
169
170        if skip:
171          continue
172
173        # FPS ranges
174        max_achievable_fps = _SEC_TO_NSEC / min_frame_duration
175        fps_params = [fps for fps in fps_ranges if (
176            fps[_MAX_FPS_INDEX] in _FPS_30_60 and
177            max_achievable_fps >= fps[_MAX_FPS_INDEX] - _FPS_SELECTION_ATOL)]
178
179        for fps_range in fps_params:
180          # HLG10. Make sure to test ON first.
181          hlg10_params = []
182          if camera_properties_utils.dynamic_range_ten_bit(props):
183            hlg10_params.append(True)
184          hlg10_params.append(False)
185
186          features_tested = []  # feature combinations already tested
187          for hlg10 in hlg10_params:
188            # Construct output surfaces
189            output_surfaces = []
190            for configured_stream in configured_streams:
191              hlg10_stream = (hlg10 and configured_stream['format'] ==
192                              its_session_utils.PRIVATE_FORMAT)
193              output_surfaces.append({'format': configured_stream['format'],
194                                      'width': configured_stream['width'],
195                                      'height': configured_stream['height'],
196                                      'hlg10': hlg10_stream})
197
198            for stabilize in stabilization_params:
199              settings = {
200                  'android.control.videoStabilizationMode': stabilize,
201                  'android.control.aeTargetFpsRange': fps_range,
202              }
203              combination_name = (f'streams_{streams_name}_hlg10_{hlg10}'
204                                  f'_stabilization_{stabilize}_fps_range_'
205                                  f'_{fps_range[0]}_{fps_range[1]}')
206              logging.debug('combination name: %s', combination_name)
207
208              # Is the feature combination supported?
209              if not cam.is_stream_combination_supported(
210                  output_surfaces, settings):
211                logging.debug('%s not supported', combination_name)
212                break
213
214              # If a superset of features are already tested, skip.
215              # pylint: disable=line-too-long
216              is_stabilized = (
217                  stabilize == camera_properties_utils.STABILIZATION_MODE_PREVIEW
218              )
219              skip_test = its_session_utils.check_and_update_features_tested(
220                  features_tested, hlg10, is_stabilized)
221              if skip_test:
222                continue
223
224              # Get zoom ratio range
225              session_props = cam.get_session_properties(
226                  output_surfaces, settings)
227              z_range = session_props.get('android.control.zoomRatioRange')
228
229              debug = self.debug_mode
230              z_min, z_max = float(z_range[0]), float(z_range[1])
231              camera_properties_utils.skip_unless(
232                  z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH)
233              z_max = min(z_max, zoom_capture_utils.ZOOM_MAX_THRESH * z_min)
234              z_list = [z_min, z_max]
235              if z_min != 1:
236                z_list = np.insert(z_list, 0, 1)  # make reference zoom 1x
237              logging.debug('Testing zoom range: %s', z_list)
238
239              # do captures over zoom range and find circles with cv2
240              img_name_stem = f'{os.path.join(self.log_path, _NAME)}'
241              req = capture_request_utils.auto_capture_request()
242
243              test_data = []
244              fmt_str = configured_streams[1]['format']
245              for i, z in enumerate(z_list):
246                req['android.control.zoomRatio'] = z
247                logging.debug('zoom ratio: %.3f', z)
248                cam.do_3a(
249                    zoom_ratio=z,
250                    out_surfaces=output_surfaces,
251                    repeat_request=None,
252                    first_surface_for_3a=True
253                )
254                cap = cam.do_capture(
255                    req, output_surfaces,
256                    reuse_session=True,
257                    first_surface_for_3a=True)
258
259                img = image_processing_utils.convert_capture_to_rgb_image(
260                    cap, props=props)
261                img_name = (f'{img_name_stem}_{combination_name}_{fmt_str}'
262                            f'_{z:.2f}.{zoom_capture_utils.JPEG_STR}')
263                image_processing_utils.write_image(img, img_name)
264
265                # determine radius tolerance of capture
266                cap_fl = cap['metadata']['android.lens.focalLength']
267                radius_tol, offset_tol = test_tols.get(
268                    cap_fl,
269                    (zoom_capture_utils.RADIUS_RTOL,
270                     zoom_capture_utils.OFFSET_RTOL)
271                )
272
273                # Scale circlish RTOL for low zoom ratios
274                if z < 1:
275                  circlish_rtol = _CIRCLISH_RTOL / z
276                else:
277                  circlish_rtol = _CIRCLISH_RTOL
278
279                # Find the center circle in img and check if it's cropped
280                circle = zoom_capture_utils.find_center_circle(
281                    img, img_name, size, z, z_list[0],
282                    circlish_rtol=circlish_rtol, debug=debug)
283
284                # Zoom is too large to find center circle
285                if circle is None:
286                  break
287                test_data.append(
288                    zoom_capture_utils.ZoomTestData(
289                        result_zoom=z,
290                        circle=circle,
291                        radius_tol=radius_tol,
292                        offset_tol=offset_tol,
293                        focal_length=cap_fl
294                    )
295                )
296
297              if not zoom_capture_utils.verify_zoom_results(
298                  test_data, size, z_max, z_min):
299                failure_msg = (
300                    f'{combination_name}: failed!'
301                    'Check test_log.DEBUG for errors')
302                test_failures.append(failure_msg)
303
304      if test_failures:
305        raise AssertionError(test_failures)
306
307if __name__ == '__main__':
308  test_runner.main()
309