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