xref: /aosp_15_r20/cts/apps/CameraITS/tests/scene_flash/test_preview_min_frame_rate.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1# Copyright 2022 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"""Verifies that preview FPS reaches minimum under low light conditions."""
15
16
17import logging
18import math
19import os.path
20
21from mobly import test_runner
22import numpy as np
23
24import its_base_test
25import camera_properties_utils
26import capture_request_utils
27import image_processing_utils
28import its_session_utils
29import lighting_control_utils
30import opencv_processing_utils
31import video_processing_utils
32
33_NAME = os.path.splitext(os.path.basename(__file__))[0]
34_PREVIEW_RECORDING_DURATION_SECONDS = 10
35_MAX_VAR_FRAME_DELTA = 0.001  # variance of frame deltas, units: seconds^2
36_FPS_ATOL = 0.8
37_DARKNESS_ATOL = 0.1 * 255  # openCV uses [0:255] images
38
39
40class PreviewMinFrameRateTest(its_base_test.ItsBaseTest):
41  """Tests preview frame rate under dark lighting conditions.
42
43  Takes preview recording under dark conditions while setting
44  CONTROL_AE_TARGET_FPS_RANGE, and checks that the
45  recording's frame rate is at the minimum of the requested FPS range.
46  """
47
48  def test_preview_min_frame_rate(self):
49    with its_session_utils.ItsSession(
50        device_id=self.dut.serial,
51        camera_id=self.camera_id,
52        hidden_physical_id=self.hidden_physical_id) as cam:
53      props = cam.get_camera_properties()
54      props = cam.override_with_hidden_physical_camera_props(props)
55
56      # check SKIP conditions
57      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
58      camera_properties_utils.skip_unless(
59          first_api_level >= its_session_utils.ANDROID14_API_LEVEL)
60
61      # determine acceptable ranges
62      fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props)
63      ae_target_fps_range = camera_properties_utils.get_fps_range_to_test(
64          fps_ranges)
65
66      # establish connection with lighting controller
67      arduino_serial_port = lighting_control_utils.lighting_control(
68          self.lighting_cntl, self.lighting_ch)
69
70      # turn OFF lights to darken scene
71      lighting_control_utils.set_lighting_state(
72          arduino_serial_port, self.lighting_ch, 'OFF')
73
74      # turn OFF DUT to reduce reflections
75      lighting_control_utils.turn_off_device_screen(self.dut)
76
77      # Validate lighting
78      cam.do_3a(do_af=False)
79      cap = cam.do_capture(
80          capture_request_utils.auto_capture_request(), cam.CAP_YUV)
81      y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap)
82      # In the sensor fusion rig, there is no tablet, so tablet_state is OFF.
83      its_session_utils.validate_lighting(
84          y_plane, self.scene, state='OFF', tablet_state='OFF',
85          log_path=self.log_path)
86      # Check for flickering frequency
87      scene_flicker_freq = cap['metadata']['android.statistics.sceneFlicker']
88      logging.debug('Detected flickering frequency: %d', scene_flicker_freq)
89      logging.debug('Taking preview recording in darkened scene.')
90      # determine camera capabilities for preview
91      preview_sizes = cam.get_supported_preview_sizes(
92          self.camera_id)
93      supported_video_sizes = cam.get_supported_video_sizes_capped(
94          self.camera_id)
95      max_video_size = supported_video_sizes[-1]  # largest available size
96      logging.debug('Camera supported video sizes: %s', supported_video_sizes)
97
98      preview_size = preview_sizes[-1]  # choose largest available size
99      if preview_size <= max_video_size:
100        logging.debug('preview_size is supported by video encoder')
101      else:
102        preview_size = max_video_size
103      logging.debug('Doing 3A to ensure AE convergence')
104      cam.do_3a(do_af=False)
105      logging.debug('Testing preview recording FPS for size: %s', preview_size)
106      preview_recording_obj = cam.do_preview_recording(
107          preview_size, _PREVIEW_RECORDING_DURATION_SECONDS, stabilize=False,
108          zoom_ratio=None,
109          ae_target_fps_min=ae_target_fps_range[0],
110          ae_target_fps_max=ae_target_fps_range[1],
111          antibanding_mode=scene_flicker_freq
112      )
113      logging.debug('preview_recording_obj: %s', preview_recording_obj)
114
115      # turn lights back ON
116      lighting_control_utils.set_lighting_state(
117          arduino_serial_port, self.lighting_ch, 'ON')
118
119      # pull the video recording file from the device.
120      self.dut.adb.pull([preview_recording_obj['recordedOutputPath'],
121                         self.log_path])
122      logging.debug('Recorded preview video is available at: %s',
123                    self.log_path)
124      preview_file_name = preview_recording_obj[
125          'recordedOutputPath'].split('/')[-1]
126      logging.debug('preview_file_name: %s', preview_file_name)
127      preview_file_name_with_path = os.path.join(
128          self.log_path, preview_file_name)
129      preview_frame_rate = video_processing_utils.get_avg_frame_rate(
130          preview_file_name_with_path)
131      errors = []
132      if not math.isclose(
133          preview_frame_rate, ae_target_fps_range[0], abs_tol=_FPS_ATOL):
134        errors.append(
135            f'Preview frame rate was {preview_frame_rate:.3f}. '
136            f'Expected to be {ae_target_fps_range[0]}, ATOL: {_FPS_ATOL}.'
137        )
138      frame_deltas = np.array(video_processing_utils.get_frame_deltas(
139          preview_file_name_with_path))
140      frame_delta_avg = np.average(frame_deltas)
141      frame_delta_var = np.var(frame_deltas)
142      logging.debug('Delta avg: %.4f, delta var: %.4f',
143                    frame_delta_avg, frame_delta_var)
144      if frame_delta_var > _MAX_VAR_FRAME_DELTA:
145        errors.append(
146            f'Preview frame delta variance {frame_delta_var:.3f} too large, '
147            f'maximum allowed: {_MAX_VAR_FRAME_DELTA}.'
148        )
149      if errors:
150        raise AssertionError('\n'.join(errors))
151
152      last_key_frame = video_processing_utils.extract_key_frames_from_video(
153          self.log_path, preview_file_name)[-1]
154      logging.debug('Confirming video brightness in frame %s is low enough.',
155                    last_key_frame)
156      last_image = image_processing_utils.convert_image_to_numpy_array(
157          os.path.join(self.log_path, last_key_frame))
158      y_avg = np.average(
159          opencv_processing_utils.convert_to_y(last_image, 'RGB')
160      )
161      logging.debug('Last frame y avg: %.4f', y_avg)
162      if not math.isclose(y_avg, 0, abs_tol=_DARKNESS_ATOL):
163        raise AssertionError(f'Last frame y average: {y_avg}, expected: 0, '
164                             f'ATOL: {_DARKNESS_ATOL}')
165
166if __name__ == '__main__':
167  test_runner.main()
168