• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 low light boost api is activated correctly when requested."""
15
16
17import cv2
18import logging
19import os.path
20import time
21
22from mobly import test_runner
23import numpy as np
24
25import its_base_test
26import camera_properties_utils
27import capture_request_utils
28import image_processing_utils
29import its_session_utils
30import lighting_control_utils
31import low_light_utils
32import preview_processing_utils
33
34_AE_LOW_LIGHT_BOOST_MODE = 6
35
36_CONTROL_AF_MODE_AUTO = 1
37_CONTROL_AWB_MODE_AUTO = 1
38_CONTROL_MODE_AUTO = 1
39_CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0
40_LENS_OPTICAL_STABILIZATION_MODE_OFF = 0
41
42_EXTENSION_NIGHT = 4  # CameraExtensionCharacteristics#EXTENSION_NIGHT
43_EXTENSION_NONE = -1  # Use Camera2 instead of a Camera Extension
44_NAME = os.path.splitext(os.path.basename(__file__))[0]
45_NUM_FRAMES_TO_WAIT = 40  # The preview frame number to capture
46_BRIGHTNESS_SETTING_CHANGE_WAIT_SEC = 5  # Seconds
47_MAX_NUM_BRIGHTEST_SQUARES = 2
48
49_AVG_DELTA_LUMINANCE_THRESH = 18
50_AVG_DELTA_LUMINANCE_THRESH_METERED_REGION = 17
51_AVG_LUMINANCE_THRESH = 70
52_AVG_LUMINANCE_THRESH_METERED_REGION = 54
53
54_CAPTURE_REQUEST = {
55    'android.control.mode': _CONTROL_MODE_AUTO,
56    'android.control.aeMode': _AE_LOW_LIGHT_BOOST_MODE,
57    'android.control.awbMode': _CONTROL_AWB_MODE_AUTO,
58    'android.control.afMode': _CONTROL_AF_MODE_AUTO,
59    'android.lens.opticalStabilizationMode':
60        _LENS_OPTICAL_STABILIZATION_MODE_OFF,
61    'android.control.videoStabilizationMode':
62        _CONTROL_VIDEO_STABILIZATION_MODE_OFF,
63}
64
65
66def _capture_and_analyze(cam, file_stem, camera_id, preview_size, extension,
67                         mirror_output, metering_region, use_metering_region,
68                         first_api_level):
69  """Capture a preview frame and then analyze it.
70
71  Args:
72    cam: ItsSession object to send commands.
73    file_stem: File prefix for captured images.
74    camera_id: Camera ID under test.
75    preview_size: Target size of preview.
76    extension: Extension mode or -1 to use Camera2.
77    mirror_output: If the output should be mirrored across the vertical axis.
78    metering_region: The metering region to use for the capture.
79    use_metering_region: Whether to use the metering region.
80    first_api_level: The first API level of the device under test.
81  """
82  luminance_thresh = _AVG_LUMINANCE_THRESH
83  delta_luminance_thresh = _AVG_DELTA_LUMINANCE_THRESH
84  capture_request = dict(_CAPTURE_REQUEST)
85  if use_metering_region and metering_region is not None:
86    logging.debug('metering_region: %s', metering_region)
87    capture_request['android.control.aeRegions'] = [metering_region]
88    capture_request['android.control.afRegions'] = [metering_region]
89    capture_request['android.control.awbRegions'] = [metering_region]
90    luminance_thresh = _AVG_LUMINANCE_THRESH_METERED_REGION
91    delta_luminance_thresh = _AVG_DELTA_LUMINANCE_THRESH_METERED_REGION
92
93  _, frame_bytes = cam.do_capture_preview_frame(
94      camera_id, preview_size, _NUM_FRAMES_TO_WAIT, extension, capture_request
95  )
96  np_array = np.frombuffer(frame_bytes, dtype=np.uint8)
97  img_rgb = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
98
99  if mirror_output:
100    img_rgb = cv2.flip(img_rgb, 1)
101  try:
102    low_light_utils.analyze_low_light_scene_capture(
103        file_stem, img_rgb, luminance_thresh, delta_luminance_thresh,
104        _MAX_NUM_BRIGHTEST_SQUARES
105    )
106  except AssertionError as e:
107    # On Android 15, we initially test without metered region. If it fails, we
108    # fallback to test with metered region. Otherwise, for newer than
109    # Android 15, we always start test with metered region.
110    if (
111        first_api_level == its_session_utils.ANDROID15_API_LEVEL
112        and not use_metering_region
113    ):
114      logging.debug('Retrying with metering region: %s', e)
115      _capture_and_analyze(cam, file_stem, camera_id, preview_size, extension,
116                           mirror_output, metering_region, True,
117                           first_api_level)
118    else:
119      raise e
120
121
122class LowLightBoostTest(its_base_test.ItsBaseTest):
123  """Tests low light boost mode under dark lighting conditions.
124
125  The test checks if low light boost AE mode is available. The test is skipped
126  if it is not available for Camera2 and Camera Extensions Night Mode.
127
128  Low light boost is enabled and a frame from the preview stream is captured
129  for analysis. The analysis applies the following operations:
130    1. Crops the region defined by a red square outline
131    2. Detects the presence of 20 boxes
132    3. Computes the luminance bounded by each box
133    4. Determines the average luminance of the 6 darkest boxes according to the
134      Hilbert curve arrangement of the grid.
135    5. Determines the average difference in luminance of the 6 successive
136      darkest boxes.
137    6. Checks for passing criteria: the avg luminance must be at least 90 or
138      greater, the avg difference in luminance between successive boxes must be
139      at least 18 or greater.
140  """
141
142  def test_low_light_boost(self):
143    self.scene = 'scene_low_light'
144    with its_session_utils.ItsSession(
145        device_id=self.dut.serial,
146        camera_id=self.camera_id,
147        hidden_physical_id=self.hidden_physical_id) as cam:
148      props = cam.get_camera_properties()
149      props = cam.override_with_hidden_physical_camera_props(props)
150      test_name = os.path.join(self.log_path, _NAME)
151
152      # Check SKIP conditions
153      # Determine if DUT is at least Android 15
154      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
155      camera_properties_utils.skip_unless(
156          first_api_level >= its_session_utils.ANDROID15_API_LEVEL)
157
158      # Determine if low light boost is available
159      is_low_light_boost_supported = (
160          cam.is_low_light_boost_available(self.camera_id, _EXTENSION_NONE))
161      is_low_light_boost_supported_night = (
162          cam.is_low_light_boost_available(self.camera_id, _EXTENSION_NIGHT))
163      should_run = (is_low_light_boost_supported or
164                    is_low_light_boost_supported_night)
165      camera_properties_utils.skip_unless(should_run)
166
167      tablet_name_unencoded = self.tablet.adb.shell(
168          ['getprop', 'ro.product.device']
169      )
170      tablet_name = str(tablet_name_unencoded.decode('utf-8')).strip()
171      logging.debug('Tablet name: %s', tablet_name)
172
173      if (tablet_name.lower() not in
174          low_light_utils.TABLET_LOW_LIGHT_SCENES_ALLOWLIST):
175        raise AssertionError('Tablet not supported for low light scenes.')
176
177      if tablet_name == its_session_utils.TABLET_LEGACY_NAME:
178        raise AssertionError(f'Incompatible tablet! Please use a tablet with '
179                             'display brightness of at least '
180                             f'{its_session_utils.TABLET_DEFAULT_BRIGHTNESS} '
181                             'according to '
182                             f'{its_session_utils.TABLET_REQUIREMENTS_URL}.')
183
184      # Establish connection with lighting controller
185      arduino_serial_port = lighting_control_utils.lighting_control(
186          self.lighting_cntl, self.lighting_ch)
187
188      # Turn OFF lights to darken scene
189      lighting_control_utils.set_lighting_state(
190          arduino_serial_port, self.lighting_ch, 'OFF')
191
192      # Check that tablet is connected and turn it off to validate lighting
193      self.turn_off_tablet()
194
195      # Turn off DUT to reduce reflections
196      lighting_control_utils.turn_off_device_screen(self.dut)
197
198      # Validate lighting, then setup tablet
199      cam.do_3a(do_af=False)
200      cap = cam.do_capture(
201          capture_request_utils.auto_capture_request(), cam.CAP_YUV)
202      y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap)
203      its_session_utils.validate_lighting(
204          y_plane, self.scene, state='OFF', log_path=self.log_path,
205          tablet_state='OFF')
206      self.setup_tablet()
207
208      its_session_utils.load_scene(
209          cam, props, self.scene, self.tablet, self.chart_distance,
210          lighting_check=False, log_path=self.log_path)
211      metering_region = low_light_utils.get_metering_region(
212          cam, f'{test_name}_{self.camera_id}')
213      use_metering_region = (
214          first_api_level > its_session_utils.ANDROID15_API_LEVEL
215      )
216
217      # Set tablet brightness to darken scene
218      props = cam.get_camera_properties()
219      brightness = low_light_utils.TABLET_BRIGHTNESS[tablet_name.lower()]
220      if (props['android.lens.facing'] ==
221          camera_properties_utils.LENS_FACING['BACK']):
222        self.set_screen_brightness(brightness[0])
223      elif (props['android.lens.facing'] ==
224            camera_properties_utils.LENS_FACING['FRONT']):
225        self.set_screen_brightness(brightness[1])
226      else:
227        logging.debug('Only front and rear camera supported. '
228                      'Skipping for camera ID %s',
229                      self.camera_id)
230        camera_properties_utils.skip_unless(False)
231
232      cam.do_3a()
233
234      # Mirror the capture across the vertical axis if captured by front facing
235      # camera
236      should_mirror = (props['android.lens.facing'] ==
237                       camera_properties_utils.LENS_FACING['FRONT'])
238
239      # Since low light boost can be supported by Camera2 and Night Mode
240      # Extensions, run the test for both (if supported)
241      # Wait for tablet brightness to change
242      time.sleep(_BRIGHTNESS_SETTING_CHANGE_WAIT_SEC)
243      if is_low_light_boost_supported:
244        # Determine preview width and height to test
245        target_preview_size = (
246            preview_processing_utils.get_max_preview_test_size(
247                cam, self.camera_id))
248        logging.debug('target_preview_size: %s', target_preview_size)
249
250        logging.debug('capture frame using camera2')
251        file_stem = f'{test_name}_{self.camera_id}_camera2'
252        _capture_and_analyze(cam, file_stem, self.camera_id,
253                             target_preview_size, _EXTENSION_NONE,
254                             should_mirror, metering_region,
255                             use_metering_region, first_api_level)
256
257      if is_low_light_boost_supported_night:
258        # Determine preview width and height to test
259        target_preview_size = (
260            preview_processing_utils.get_max_extension_preview_test_size(
261                cam, self.camera_id, _EXTENSION_NIGHT
262            )
263        )
264        logging.debug('target_preview_size: %s', target_preview_size)
265
266        logging.debug('capture frame using night mode extension')
267        file_stem = f'{test_name}_{self.camera_id}_camera_extension'
268        _capture_and_analyze(cam, file_stem, self.camera_id,
269                             target_preview_size, _EXTENSION_NIGHT,
270                             should_mirror, metering_region,
271                             use_metering_region, first_api_level)
272
273
274if __name__ == '__main__':
275  test_runner.main()
276