xref: /aosp_15_r20/cts/apps/CameraITS/tests/scene_extensions/scene_low_light/test_night_extension.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1# Copyright 2023 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 night extension is activated correctly when requested."""
15
16
17import logging
18import os.path
19
20import time
21import cv2
22from mobly import test_runner
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 low_light_utils
31
32_NAME = os.path.splitext(os.path.basename(__file__))[0]
33_EXTENSION_NIGHT = 4  # CameraExtensionCharacteristics.EXTENSION_NIGHT
34_TEST_REQUIRED_MPC = 34
35
36_AVG_DELTA_LUMINANCE_THRESH = 17
37_AVG_DELTA_LUMINANCE_THRESH_METERED_REGION = 25
38_AVG_LUMINANCE_THRESH = 85
39_AVG_LUMINANCE_THRESH_METERED_REGION = 80
40
41_IMAGE_FORMATS_TO_CONSTANTS = (('yuv', 35), ('jpeg', 256))
42
43_BRIGHTNESS_SETTING_CHANGE_WAIT_SEC = 5  # Seconds
44_X_STRING = 'x'
45
46
47def _convert_capture(cap, file_stem=None):
48  """Obtains y plane and numpy image from a capture.
49
50  Args:
51    cap: A capture object as returned by its_session_utils.do_capture.
52    file_stem: str; location and name to save files.
53  Returns:
54    numpy image, with the np.uint8 data type.
55  """
56  img = image_processing_utils.convert_capture_to_rgb_image(cap)
57  if file_stem:
58    image_processing_utils.write_image(img, f'{file_stem}.jpg')
59  return image_processing_utils.convert_image_to_uint8(img)
60
61
62class NightExtensionTest(its_base_test.ItsBaseTest):
63  """Tests night extension under dark lighting conditions.
64
65  A capture is taken with the night extension ON, after AE converges.
66  The capture is analyzed in the same way as test_low_light_boost_extension,
67  checking luminance and the average difference in luminance between
68  successive boxes.
69  """
70
71  def _take_capture(self, cam, req, out_surfaces):
72    """Takes capture with night extension ON.
73
74    Args:
75      cam: its_session_utils object.
76      req: capture request.
77      out_surfaces: dictionary of output surfaces.
78    Returns:
79      cap: capture object.
80    """
81    cap = cam.do_capture_with_extensions(req, _EXTENSION_NIGHT, out_surfaces)
82    metadata = cap['metadata']
83    logging.debug('capture exposure time: %s',
84                  metadata['android.sensor.exposureTime'])
85    logging.debug('capture sensitivity: %s',
86                  metadata['android.sensor.sensitivity'])
87    return cap
88
89  def _take_capture_and_analyze(self, cam, req, out_surfaces, file_stem,
90                                metering_region, use_metering_region,
91                                first_api_level):
92    """Takes capture with night extension ON and analyzes it.
93
94    Args:
95      cam: its_session_utils object.
96      req: capture request.
97      out_surfaces: dictionary of output surfaces.
98      file_stem: File prefix for captured images.
99      metering_region: The metering region to use for the capture.
100      use_metering_region: Whether to use the metering region.
101      first_api_level: The first API level of the device under test.
102    """
103    avg_luminance_thresh = _AVG_LUMINANCE_THRESH
104    avg_delta_luminance_thresh = _AVG_DELTA_LUMINANCE_THRESH
105    if use_metering_region and metering_region is not None:
106      logging.debug('metering_region: %s', metering_region)
107      req['android.control.aeRegions'] = [metering_region]
108      req['android.control.afRegions'] = [metering_region]
109      req['android.control.awbRegions'] = [metering_region]
110      avg_luminance_thresh = _AVG_LUMINANCE_THRESH_METERED_REGION
111      avg_delta_luminance_thresh = _AVG_DELTA_LUMINANCE_THRESH_METERED_REGION
112    cap = self._take_capture(cam, req, out_surfaces)
113    rgb_night_img = _convert_capture(cap, f'{file_stem}_night')
114
115    # Assert correct behavior and create luminosity plots
116    try:
117      low_light_utils.analyze_low_light_scene_capture(
118          f'{file_stem}_night',
119          cv2.cvtColor(rgb_night_img, cv2.COLOR_RGB2BGR),
120          avg_luminance_thresh,
121          avg_delta_luminance_thresh
122      )
123    except AssertionError as e:
124      # On Android 15, we initially test without metered region. If it fails, we
125      # fallback to test with metered region. Otherwise, for newer than
126      # Android 15, we always start test with metered region.
127      if (
128          first_api_level <= its_session_utils.ANDROID15_API_LEVEL
129          and not use_metering_region
130      ):
131        logging.debug('Retrying with metering region: %s', e)
132        self._take_capture_and_analyze(cam, req, out_surfaces, file_stem,
133                                       metering_region, True, first_api_level)
134      else:
135        raise e
136
137  def test_night_extension(self):
138    # Handle subdirectory
139    self.scene = 'scene_low_light'
140    with its_session_utils.ItsSession(
141        device_id=self.dut.serial,
142        camera_id=self.camera_id,
143        hidden_physical_id=self.hidden_physical_id) as cam:
144      props = cam.get_camera_properties()
145      props = cam.override_with_hidden_physical_camera_props(props)
146      test_name = os.path.join(self.log_path, _NAME)
147      camera_id = self.camera_id
148
149      # Determine camera supported extensions
150      supported_extensions = cam.get_supported_extensions(camera_id)
151      logging.debug('Supported extensions: %s', supported_extensions)
152
153      # Check media performance class
154      should_run = _EXTENSION_NIGHT in supported_extensions
155      media_performance_class = its_session_utils.get_media_performance_class(
156          self.dut.serial)
157      if (media_performance_class >= _TEST_REQUIRED_MPC and
158          cam.is_primary_camera() and
159          not should_run):
160        its_session_utils.raise_mpc_assertion_error(
161            _TEST_REQUIRED_MPC, _NAME, media_performance_class)
162
163      # Check SKIP conditions
164      camera_properties_utils.skip_unless(should_run)
165
166      tablet_name_unencoded = self.tablet.adb.shell(
167          ['getprop', 'ro.product.device']
168      )
169      tablet_name = str(tablet_name_unencoded.decode('utf-8')).strip()
170      logging.debug('Tablet name: %s', tablet_name)
171
172      if (tablet_name.lower() not in
173          low_light_utils.TABLET_LOW_LIGHT_SCENES_ALLOWLIST):
174        raise AssertionError('Tablet not supported for low light scenes.')
175
176      if tablet_name == its_session_utils.TABLET_LEGACY_NAME:
177        raise AssertionError(f'Incompatible tablet! Please use a tablet with '
178                             'display brightness of at least '
179                             f'{its_session_utils.TABLET_DEFAULT_BRIGHTNESS} '
180                             'according to '
181                             f'{its_session_utils.TABLET_REQUIREMENTS_URL}.')
182
183      # Establish connection with lighting controller
184      arduino_serial_port = lighting_control_utils.lighting_control(
185          self.lighting_cntl, self.lighting_ch)
186
187      # Turn OFF lights to darken scene
188      lighting_control_utils.set_lighting_state(
189          arduino_serial_port, self.lighting_ch, 'OFF')
190
191      # Check that tablet is connected and turn it off to validate lighting
192      self.turn_off_tablet()
193
194      # Turn off DUT to reduce reflections
195      lighting_control_utils.turn_off_device_screen(self.dut)
196
197      # Validate lighting, then setup tablet
198      cam.do_3a(do_af=False)
199      cap = cam.do_capture(
200          capture_request_utils.auto_capture_request(), cam.CAP_YUV)
201      y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap)
202      its_session_utils.validate_lighting(
203          y_plane, self.scene, state='OFF', log_path=self.log_path,
204          tablet_state='OFF')
205      self.setup_tablet()
206
207      its_session_utils.load_scene(
208          cam, props, self.scene, self.tablet, self.chart_distance,
209          lighting_check=False, log_path=self.log_path)
210
211      # Determine capture width, height, and format
212      for format_name, format_constant in _IMAGE_FORMATS_TO_CONSTANTS:
213        capture_sizes = capture_request_utils.get_available_output_sizes(
214            format_name, props)
215        extension_capture_sizes_str = cam.get_supported_extension_sizes(
216            camera_id, _EXTENSION_NIGHT, format_constant
217        )
218        if not extension_capture_sizes_str:
219          continue
220        extension_capture_sizes = [
221            tuple(int(size_part) for size_part in s.split(_X_STRING))
222            for s in extension_capture_sizes_str
223        ]
224        # Extension capture sizes ordered in ascending area order by default
225        extension_capture_sizes.reverse()
226        logging.debug('Capture sizes: %s', capture_sizes)
227        logging.debug('Extension capture sizes: %s', extension_capture_sizes)
228        logging.debug('Accepted capture format: %s', format_name)
229        width, height = extension_capture_sizes[0]
230        accepted_format = format_name
231        break
232      else:
233        raise AssertionError('No supported sizes/formats found!')
234
235      file_stem = (
236          f'{test_name}_{self.camera_id}_{accepted_format}_{width}x{height}'
237      )
238      out_surfaces = {
239          'format': accepted_format, 'width': width, 'height': height}
240      req = capture_request_utils.auto_capture_request()
241      metering_region = low_light_utils.get_metering_region(cam, file_stem)
242      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
243      use_metering_region = (
244          first_api_level > its_session_utils.ANDROID15_API_LEVEL
245      )
246
247      # Set tablet brightness to darken scene
248      brightness = low_light_utils.TABLET_BRIGHTNESS[tablet_name.lower()]
249      if (props['android.lens.facing'] ==
250          camera_properties_utils.LENS_FACING['BACK']):
251        self.set_screen_brightness(brightness[0])
252      elif (props['android.lens.facing'] ==
253            camera_properties_utils.LENS_FACING['FRONT']):
254        self.set_screen_brightness(brightness[1])
255      else:
256        logging.debug('Only front and rear camera supported. '
257                      'Skipping for camera ID %s',
258                      self.camera_id)
259        camera_properties_utils.skip_unless(False)
260
261      logging.debug('Taking auto capture with night mode ON')
262      # Wait for tablet brightness to change
263      time.sleep(_BRIGHTNESS_SETTING_CHANGE_WAIT_SEC)
264      self._take_capture_and_analyze(cam, req, out_surfaces, file_stem,
265                                     metering_region, use_metering_region,
266                                     first_api_level)
267
268if __name__ == '__main__':
269  test_runner.main()
270