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
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 low_light_utils
31import preview_processing_utils
32
33_AE_LOW_LIGHT_BOOST_MODE = 6
34
35_CONTROL_AF_MODE_AUTO = 1
36_CONTROL_AWB_MODE_AUTO = 1
37_CONTROL_MODE_AUTO = 1
38_CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0
39_LENS_OPTICAL_STABILIZATION_MODE_OFF = 0
40
41_EXTENSION_NIGHT = 4  # CameraExtensionCharacteristics#EXTENSION_NIGHT
42_EXTENSION_NONE = -1  # Use Camera2 instead of a Camera Extension
43_NAME = os.path.splitext(os.path.basename(__file__))[0]
44_NUM_FRAMES_TO_WAIT = 40  # The preview frame number to capture
45_TABLET_BRIGHTNESS_REAR_CAMERA = '6'  # Target brightness on a supported tablet
46_TABLET_BRIGHTNESS_FRONT_CAMERA = '12'  # Target brightness on a supported
47                                        # tablet
48_TAP_COORDINATES = (500, 500)  # Location to tap tablet screen via adb
49
50_AVG_DELTA_LUMINANCE_THRESH = 18
51_AVG_LUMINANCE_THRESH = 70
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):
67  """Capture a preview frame and then analyze it.
68
69  Args:
70    cam: ItsSession object to send commands.
71    file_stem: File prefix for captured images.
72    camera_id: Camera ID under test.
73    preview_size: Target size of preview.
74    extension: Extension mode or -1 to use Camera2.
75    mirror_output: If the output should be mirrored across the vertical axis.
76  """
77  frame_bytes = cam.do_capture_preview_frame(camera_id,
78                                             preview_size,
79                                             _NUM_FRAMES_TO_WAIT,
80                                             extension,
81                                             _CAPTURE_REQUEST)
82  np_array = np.frombuffer(frame_bytes, dtype=np.uint8)
83  img_rgb = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
84  if mirror_output:
85    img_rgb = cv2.flip(img_rgb, 1)
86  low_light_utils.analyze_low_light_scene_capture(
87      file_stem,
88      img_rgb,
89      _AVG_LUMINANCE_THRESH,
90      _AVG_DELTA_LUMINANCE_THRESH)
91
92
93class LowLightBoostTest(its_base_test.ItsBaseTest):
94  """Tests low light boost mode under dark lighting conditions.
95
96  The test checks if low light boost AE mode is available. The test is skipped
97  if it is not available for Camera2 and Camera Extensions Night Mode.
98
99  Low light boost is enabled and a frame from the preview stream is captured
100  for analysis. The analysis applies the following operations:
101    1. Crops the region defined by a red square outline
102    2. Detects the presence of 20 boxes
103    3. Computes the luminance bounded by each box
104    4. Determines the average luminance of the 6 darkest boxes according to the
105      Hilbert curve arrangement of the grid.
106    5. Determines the average difference in luminance of the 6 successive
107      darkest boxes.
108    6. Checks for passing criteria: the avg luminance must be at least 90 or
109      greater, the avg difference in luminance between successive boxes must be
110      at least 18 or greater.
111  """
112
113  def test_low_light_boost(self):
114    self.scene = 'scene_low_light'
115    with its_session_utils.ItsSession(
116        device_id=self.dut.serial,
117        camera_id=self.camera_id,
118        hidden_physical_id=self.hidden_physical_id) as cam:
119      props = cam.get_camera_properties()
120      props = cam.override_with_hidden_physical_camera_props(props)
121      test_name = os.path.join(self.log_path, _NAME)
122
123      # Check SKIP conditions
124      # Determine if DUT is at least Android 15
125      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
126      camera_properties_utils.skip_unless(
127          first_api_level >= its_session_utils.ANDROID15_API_LEVEL)
128
129      # Determine if low light boost is available
130      is_low_light_boost_supported = (
131          cam.is_low_light_boost_available(self.camera_id, _EXTENSION_NONE))
132      is_low_light_boost_supported_night = (
133          cam.is_low_light_boost_available(self.camera_id, _EXTENSION_NIGHT))
134      should_run = (is_low_light_boost_supported or
135                    is_low_light_boost_supported_night)
136      camera_properties_utils.skip_unless(should_run)
137
138      tablet_name_unencoded = self.tablet.adb.shell(
139          ['getprop', 'ro.product.device']
140      )
141      tablet_name = str(tablet_name_unencoded.decode('utf-8')).strip()
142      logging.debug('Tablet name: %s', tablet_name)
143
144      if (tablet_name.lower() not in
145          low_light_utils.TABLET_LOW_LIGHT_SCENES_ALLOWLIST):
146        raise AssertionError('Tablet not supported for low light scenes.')
147
148      if tablet_name == its_session_utils.TABLET_LEGACY_NAME:
149        raise AssertionError(f'Incompatible tablet! Please use a tablet with '
150                             'display brightness of at least '
151                             f'{its_session_utils.TABLET_DEFAULT_BRIGHTNESS} '
152                             'according to '
153                             f'{its_session_utils.TABLET_REQUIREMENTS_URL}.')
154
155      # Establish connection with lighting controller
156      arduino_serial_port = lighting_control_utils.lighting_control(
157          self.lighting_cntl, self.lighting_ch)
158
159      # Turn OFF lights to darken scene
160      lighting_control_utils.set_lighting_state(
161          arduino_serial_port, self.lighting_ch, 'OFF')
162
163      # Check that tablet is connected and turn it off to validate lighting
164      self.turn_off_tablet()
165
166      # Turn off DUT to reduce reflections
167      lighting_control_utils.turn_off_device_screen(self.dut)
168
169      # Validate lighting, then setup tablet
170      cam.do_3a(do_af=False)
171      cap = cam.do_capture(
172          capture_request_utils.auto_capture_request(), cam.CAP_YUV)
173      y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap)
174      its_session_utils.validate_lighting(
175          y_plane, self.scene, state='OFF', log_path=self.log_path,
176          tablet_state='OFF')
177      self.setup_tablet()
178
179      its_session_utils.load_scene(
180          cam, props, self.scene, self.tablet, self.chart_distance,
181          lighting_check=False, log_path=self.log_path)
182
183      # Tap tablet to remove gallery buttons
184      if self.tablet:
185        self.tablet.adb.shell(
186            f'input tap {_TAP_COORDINATES[0]} {_TAP_COORDINATES[1]}')
187
188      # Set tablet brightness to darken scene
189      props = cam.get_camera_properties()
190      if (props['android.lens.facing'] ==
191          camera_properties_utils.LENS_FACING['BACK']):
192        self.set_screen_brightness(_TABLET_BRIGHTNESS_REAR_CAMERA)
193      elif (props['android.lens.facing'] ==
194            camera_properties_utils.LENS_FACING['FRONT']):
195        self.set_screen_brightness(_TABLET_BRIGHTNESS_FRONT_CAMERA)
196      else:
197        logging.debug('Only front and rear camera supported. '
198                      'Skipping for camera ID %s',
199                      self.camera_id)
200        camera_properties_utils.skip_unless(False)
201
202      cam.do_3a()
203
204      # Mirror the capture across the vertical axis if captured by front facing
205      # camera
206      should_mirror = (props['android.lens.facing'] ==
207                       camera_properties_utils.LENS_FACING['FRONT'])
208
209      # Since low light boost can be supported by Camera2 and Night Mode
210      # Extensions, run the test for both (if supported)
211
212      if is_low_light_boost_supported:
213        # Determine preview width and height to test
214        target_preview_size = (
215            preview_processing_utils.get_max_preview_test_size(
216                cam, self.camera_id))
217        logging.debug('target_preview_size: %s', target_preview_size)
218
219        logging.debug('capture frame using camera2')
220        file_stem = f'{test_name}_{self.camera_id}_camera2'
221        _capture_and_analyze(cam, file_stem, self.camera_id,
222                             target_preview_size, _EXTENSION_NONE,
223                             should_mirror)
224
225      if is_low_light_boost_supported_night:
226        # Determine preview width and height to test
227        target_preview_size = (
228            preview_processing_utils.get_max_extension_preview_test_size(
229                cam, self.camera_id, _EXTENSION_NIGHT))
230        logging.debug('target_preview_size: %s', target_preview_size)
231
232        logging.debug('capture frame using night mode extension')
233        file_stem = f'{test_name}_{self.camera_id}_camera_extension'
234        _capture_and_analyze(cam, file_stem, self.camera_id,
235                             target_preview_size, _EXTENSION_NIGHT,
236                             should_mirror)
237
238
239if __name__ == '__main__':
240  test_runner.main()
241