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 feature combinations for stabilization, 10-bit, and frame rate."""
15
16import logging
17import os
18
19from mobly import test_runner
20
21import its_base_test
22import camera_properties_utils
23import capture_request_utils
24import its_session_utils
25import preview_processing_utils
26import video_processing_utils
27
28_BIT_HLG10 = 0x01  # bit 1 for feature mask
29_BIT_STABILIZATION = 0x02  # bit 2 for feature mask
30_FPS_30_60 = (30, 60)
31_FPS_SELECTION_ATOL = 0.01
32_FPS_ATOL_CODEC = 1.2
33_FPS_ATOL_METADATA = 0.8
34
35_NAME = os.path.splitext(os.path.basename(__file__))[0]
36_SEC_TO_NSEC = 1_000_000_000
37
38
39class FeatureCombinationTest(its_base_test.ItsBaseTest):
40  """Tests camera feature combinations.
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  Camera is moved in sensor fusion rig on an arc of 15 degrees.
48  Speed is set to mimic hand movement (and not be too fast).
49  Preview is captured after rotation rig starts moving and the
50  gyroscope data is dumped.
51
52  Preview stabilization:
53  The recorded preview is processed to dump all of the frames to
54  PNG files. Camera movement is extracted from frames by determining
55  max angle of deflection in video movement vs max angle of deflection
56  in gyroscope movement. Test is a PASS if rotation is reduced in video.
57
58  Target FPS range:
59  The recorded preview has the expected fps range. For example,
60  if [60, 60] is set as targetFpsRange, the camera device is expected to
61  produce 60fps preview/video.
62
63  HLG 10-bit HDR:
64  The recorded preview has valid 10-bit HLG outputs.
65  """
66
67  def test_feature_combination(self):
68    rot_rig = {}
69    log_path = self.log_path
70
71    with its_session_utils.ItsSession(
72        device_id=self.dut.serial,
73        camera_id=self.camera_id) as cam:
74
75      # Skip if the device doesn't support feature combination query
76      props = cam.get_camera_properties()
77      feature_combination_query_version = props.get(
78          'android.info.sessionConfigurationQueryVersion')
79      if not feature_combination_query_version:
80        feature_combination_query_version = (
81            its_session_utils.ANDROID14_API_LEVEL
82        )
83      should_run = (feature_combination_query_version >=
84                    its_session_utils.ANDROID15_API_LEVEL)
85      camera_properties_utils.skip_unless(should_run)
86
87      # Log ffmpeg version being used
88      video_processing_utils.log_ffmpeg_version()
89
90      # Raise error if not FRONT or REAR facing camera
91      facing = props['android.lens.facing']
92      camera_properties_utils.check_front_or_rear_camera(props)
93
94      # Initialize rotation rig
95      rot_rig['cntl'] = self.rotator_cntl
96      rot_rig['ch'] = self.rotator_ch
97      if rot_rig['cntl'].lower() != 'arduino':
98        raise AssertionError(
99            f'You must use the arduino controller for {_NAME}.')
100
101      # List of queryable stream combinations
102      combinations_str, combinations = cam.get_queryable_stream_combinations()
103      logging.debug('Queryable stream combinations: %s', combinations_str)
104
105      # Stabilization modes. Make sure to test ON first.
106      stabilization_params = []
107      stabilization_modes = props[
108          'android.control.availableVideoStabilizationModes']
109      if (camera_properties_utils.STABILIZATION_MODE_PREVIEW in
110          stabilization_modes):
111        stabilization_params.append(
112            camera_properties_utils.STABILIZATION_MODE_PREVIEW)
113      stabilization_params.append(
114          camera_properties_utils.STABILIZATION_MODE_OFF
115      )
116      logging.debug('stabilization modes: %s', stabilization_params)
117
118      configs = props['android.scaler.streamConfigurationMap'][
119          'availableStreamConfigurations']
120      fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props)
121
122      test_failures = []
123      for stream_combination in combinations:
124        streams_name = stream_combination['name']
125        min_frame_duration = 0
126        configured_streams = []
127        skip = False
128        if (stream_combination['combination'][0]['format'] !=
129            its_session_utils.PRIVATE_FORMAT):
130          raise AssertionError(
131              f'First stream for {streams_name} must be PRIV')
132        preview_size = stream_combination['combination'][0]['size']
133        for stream in stream_combination['combination']:
134          fmt = None
135          size = [int(e) for e in stream['size'].split('x')]
136          if stream['format'] == its_session_utils.PRIVATE_FORMAT:
137            fmt = capture_request_utils.FMT_CODE_PRIV
138          elif stream['format'] == 'jpeg':
139            fmt = capture_request_utils.FMT_CODE_JPEG
140          elif stream['format'] == its_session_utils.JPEG_R_FMT_STR:
141            fmt = capture_request_utils.FMT_CODE_JPEG_R
142          config = [x for x in configs if
143                    x['format'] == fmt and
144                    x['width'] == size[0] and
145                    x['height'] == size[1]]
146          if not config:
147            logging.debug(
148                'stream combination %s not supported. Skip', streams_name)
149            skip = True
150            break
151
152          min_frame_duration = max(
153              config[0]['minFrameDuration'], min_frame_duration)
154          logging.debug(
155              'format is %s, min_frame_duration is %d}',
156              stream['format'], config[0]['minFrameDuration'])
157          configured_streams.append(
158              {'format': stream['format'], 'width': size[0], 'height': size[1]})
159
160        if skip:
161          continue
162
163        # Fps ranges
164        max_achievable_fps = _SEC_TO_NSEC / min_frame_duration
165        fps_params = [fps for fps in fps_ranges if (
166            fps[1] in _FPS_30_60 and
167            max_achievable_fps >= fps[1] - _FPS_SELECTION_ATOL)]
168
169        for fps_range in fps_params:
170          # HLG10. Make sure to test ON first.
171          hlg10_params = []
172          if cam.is_hlg10_recording_supported_for_size_and_fps(
173              preview_size, fps_range[1]):
174            hlg10_params.append(True)
175          hlg10_params.append(False)
176
177          features_tested = []  # feature combinations already tested
178          for hlg10 in hlg10_params:
179            # Construct output surfaces
180            output_surfaces = []
181            for configured_stream in configured_streams:
182              hlg10_stream = (configured_stream['format'] ==
183                              its_session_utils.PRIVATE_FORMAT and hlg10)
184              output_surfaces.append({'format': configured_stream['format'],
185                                      'width': configured_stream['width'],
186                                      'height': configured_stream['height'],
187                                      'hlg10': hlg10_stream})
188
189            for stabilize in stabilization_params:
190              settings = {
191                  'android.control.videoStabilizationMode': stabilize,
192                  'android.control.aeTargetFpsRange': fps_range,
193              }
194              combination_name = (f'(streams: {streams_name}, hlg10: {hlg10}, '
195                                  f'stabilization: {stabilize}, fps_range: '
196                                  f'[{fps_range[0]}, {fps_range[1]}])')
197              logging.debug('combination name: %s', combination_name)
198
199              # Is the feature combination supported?
200              supported = cam.is_stream_combination_supported(
201                  output_surfaces, settings)
202              if not supported:
203                logging.debug('%s not supported', combination_name)
204                break
205
206              is_stabilized = False
207              if (stabilize ==
208                  camera_properties_utils.STABILIZATION_MODE_PREVIEW):
209                is_stabilized = True
210
211              # If a superset of features are already tested, skip.
212              skip_test = its_session_utils.check_and_update_features_tested(
213                  features_tested, hlg10, is_stabilized)
214              if skip_test:
215                continue
216
217              recording_obj = (
218                  preview_processing_utils.collect_data_with_surfaces(
219                      cam, self.tablet_device, output_surfaces, is_stabilized,
220                      rot_rig=rot_rig, fps_range=fps_range))
221
222              if is_stabilized:
223                # Get gyro events
224                logging.debug('Reading out inertial sensor events')
225                gyro_events = cam.get_sensor_events()['gyro']
226                logging.debug('Number of gyro samples %d', len(gyro_events))
227
228              # Grab the video from the file location on DUT
229              self.dut.adb.pull([recording_obj['recordedOutputPath'], log_path])
230
231              # Verify FPS by inspecting the video clip
232              preview_file_name = (
233                  recording_obj['recordedOutputPath'].split('/')[-1])
234              preview_file_name_with_path = os.path.join(
235                  self.log_path, preview_file_name)
236              average_frame_rate_codec = (
237                  video_processing_utils.get_average_frame_rate(
238                      preview_file_name_with_path))
239              logging.debug('Average codec frame rate for %s is %f', combination_name,
240                            average_frame_rate_codec)
241              if (average_frame_rate_codec > fps_range[1] + _FPS_ATOL_CODEC or
242                  average_frame_rate_codec < fps_range[0] - _FPS_ATOL_CODEC):
243                failure_msg = (
244                    f'{combination_name}: Average video clip frame rate '
245                    f'{average_frame_rate_codec} exceeding the allowed range of '
246                    f'({fps_range[0]}-{_FPS_ATOL_CODEC}, '
247                    f'{fps_range[1]}+{_FPS_ATOL_CODEC})')
248                test_failures.append(failure_msg)
249
250              # Verify FPS by inspecting the result metadata
251              capture_results = recording_obj['captureMetadata'];
252              assert len(capture_results) > 1
253              last_t = capture_results[-1]['android.sensor.timestamp'];
254              first_t = capture_results[0]['android.sensor.timestamp'];
255              average_frame_duration = (last_t - first_t) / (len(capture_results) - 1)
256              average_frame_rate_metadata = _SEC_TO_NSEC / average_frame_duration
257              logging.debug('Average metadata frame rate for %s is %f', combination_name,
258                            average_frame_rate_metadata)
259              if (average_frame_rate_metadata > fps_range[1] + _FPS_ATOL_METADATA or
260                  average_frame_rate_metadata < fps_range[0] - _FPS_ATOL_METADATA):
261                failure_msg = (
262                    f'{combination_name}: Average frame rate '
263                    f'{average_frame_rate_metadata} exceeding the allowed range of '
264                    f'({fps_range[0]}-{_FPS_ATOL_METADATA}, {fps_range[1]}+{_FPS_ATOL_METADATA})')
265                test_failures.append(failure_msg)
266
267              # Verify video stabilization
268              if is_stabilized:
269                stabilization_result = (
270                    preview_processing_utils.verify_preview_stabilization(
271                        recording_obj, gyro_events, _NAME, log_path, facing))
272                if stabilization_result['failure'] is not None:
273                  failure_msg = (combination_name + ': ' +
274                                 stabilization_result['failure'])
275                  test_failures.append(failure_msg)
276
277              # Verify color space
278              color_space = video_processing_utils.get_video_colorspace(
279                  self.log_path, preview_file_name_with_path)
280              if (hlg10 and
281                  video_processing_utils.COLORSPACE_HDR not in color_space):
282                failure_msg = (
283                    f'{combination_name}: video color space {color_space} '
284                    'is missing COLORSPACE_HDR')
285                test_failures.append(failure_msg)
286
287      # Assert PASS/FAIL criteria
288      if test_failures:
289        raise AssertionError(test_failures)
290
291if __name__ == '__main__':
292  test_runner.main()
293