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 cv2
21from mobly import test_runner
22
23import its_base_test
24import camera_properties_utils
25import capture_request_utils
26import image_processing_utils
27import its_session_utils
28import lighting_control_utils
29import low_light_utils
30
31_NAME = os.path.splitext(os.path.basename(__file__))[0]
32_EXTENSION_NIGHT = 4  # CameraExtensionCharacteristics.EXTENSION_NIGHT
33_TABLET_BRIGHTNESS = '6'  # Highest minimum brightness on a supported tablet
34_TAP_COORDINATES = (500, 500)  # Location to tap tablet screen via adb
35_TEST_REQUIRED_MPC = 34
36
37_AVG_DELTA_LUMINANCE_THRESH = 17
38_AVG_LUMINANCE_THRESH = 90
39
40_IMAGE_FORMATS_TO_CONSTANTS = (('yuv', 35), ('jpeg', 256))
41
42_X_STRING = 'x'
43
44
45def _convert_capture(cap, file_stem=None):
46  """Obtains y plane and numpy image from a capture.
47
48  Args:
49    cap: A capture object as returned by its_session_utils.do_capture.
50    file_stem: str; location and name to save files.
51  Returns:
52    numpy image, with the np.uint8 data type.
53  """
54  img = image_processing_utils.convert_capture_to_rgb_image(cap)
55  if file_stem:
56    image_processing_utils.write_image(img, f'{file_stem}.jpg')
57  return image_processing_utils.convert_image_to_uint8(img)
58
59
60class NightExtensionTest(its_base_test.ItsBaseTest):
61  """Tests night extension under dark lighting conditions.
62
63  A capture is taken with the night extension ON, after AE converges.
64  The capture is analyzed in the same way as test_low_light_boost_extension,
65  checking luminance and the average difference in luminance between
66  successive boxes.
67  """
68
69  def _take_capture(self, cam, req, out_surfaces):
70    """Takes capture with Night extension ON.
71
72    Args:
73      cam: its_session_utils object.
74      req: capture request.
75      out_surfaces: dictionary of output surfaces.
76    Returns:
77      cap: capture object.
78    """
79    cap = cam.do_capture_with_extensions(req, _EXTENSION_NIGHT, out_surfaces)
80    metadata = cap['metadata']
81    logging.debug('capture exposure time: %s',
82                  metadata['android.sensor.exposureTime'])
83    logging.debug('capture sensitivity: %s',
84                  metadata['android.sensor.sensitivity'])
85    return cap
86
87  def test_night_extension(self):
88    # Handle subdirectory
89    self.scene = 'scene_low_light'
90    with its_session_utils.ItsSession(
91        device_id=self.dut.serial,
92        camera_id=self.camera_id,
93        hidden_physical_id=self.hidden_physical_id) as cam:
94      props = cam.get_camera_properties()
95      props = cam.override_with_hidden_physical_camera_props(props)
96      test_name = os.path.join(self.log_path, _NAME)
97
98      # Determine camera supported extensions
99      supported_extensions = cam.get_supported_extensions(self.camera_id)
100      logging.debug('Supported extensions: %s', supported_extensions)
101
102      # Check media performance class
103      should_run = _EXTENSION_NIGHT in supported_extensions
104      media_performance_class = its_session_utils.get_media_performance_class(
105          self.dut.serial)
106      if (media_performance_class >= _TEST_REQUIRED_MPC and
107          cam.is_primary_camera() and
108          not should_run):
109        its_session_utils.raise_mpc_assertion_error(
110            _TEST_REQUIRED_MPC, _NAME, media_performance_class)
111
112      # Check SKIP conditions
113      camera_properties_utils.skip_unless(should_run)
114
115      tablet_name_unencoded = self.tablet.adb.shell(
116          ['getprop', 'ro.product.device']
117      )
118      tablet_name = str(tablet_name_unencoded.decode('utf-8')).strip()
119      logging.debug('Tablet name: %s', tablet_name)
120
121      if (tablet_name.lower() not in
122          low_light_utils.TABLET_LOW_LIGHT_SCENES_ALLOWLIST):
123        raise AssertionError('Tablet not supported for low light scenes.')
124
125      if tablet_name == its_session_utils.TABLET_LEGACY_NAME:
126        raise AssertionError(f'Incompatible tablet! Please use a tablet with '
127                             'display brightness of at least '
128                             f'{its_session_utils.TABLET_DEFAULT_BRIGHTNESS} '
129                             'according to '
130                             f'{its_session_utils.TABLET_REQUIREMENTS_URL}.')
131
132      # Establish connection with lighting controller
133      arduino_serial_port = lighting_control_utils.lighting_control(
134          self.lighting_cntl, self.lighting_ch)
135
136      # Turn OFF lights to darken scene
137      lighting_control_utils.set_lighting_state(
138          arduino_serial_port, self.lighting_ch, 'OFF')
139
140      # Check that tablet is connected and turn it off to validate lighting
141      self.turn_off_tablet()
142
143      # Turn off DUT to reduce reflections
144      lighting_control_utils.turn_off_device_screen(self.dut)
145
146      # Validate lighting, then setup tablet
147      cam.do_3a(do_af=False)
148      cap = cam.do_capture(
149          capture_request_utils.auto_capture_request(), cam.CAP_YUV)
150      y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap)
151      its_session_utils.validate_lighting(
152          y_plane, self.scene, state='OFF', log_path=self.log_path,
153          tablet_state='OFF')
154      self.setup_tablet()
155
156      its_session_utils.load_scene(
157          cam, props, self.scene, self.tablet, self.chart_distance,
158          lighting_check=False, log_path=self.log_path)
159
160      # Tap tablet to remove gallery buttons
161      if self.tablet:
162        self.tablet.adb.shell(
163            f'input tap {_TAP_COORDINATES[0]} {_TAP_COORDINATES[1]}')
164
165      # Determine capture width, height, and format
166      for format_name, format_constant in _IMAGE_FORMATS_TO_CONSTANTS:
167        capture_sizes = capture_request_utils.get_available_output_sizes(
168            format_name, props)
169        extension_capture_sizes_str = cam.get_supported_extension_sizes(
170            self.camera_id, _EXTENSION_NIGHT, format_constant
171        )
172        if not extension_capture_sizes_str:
173          continue
174        extension_capture_sizes = [
175            tuple(int(size_part) for size_part in s.split(_X_STRING))
176            for s in extension_capture_sizes_str
177        ]
178        # Extension capture sizes ordered in ascending area order by default
179        extension_capture_sizes.reverse()
180        logging.debug('Capture sizes: %s', capture_sizes)
181        logging.debug('Extension capture sizes: %s', extension_capture_sizes)
182        logging.debug('Accepted capture format: %s', format_name)
183        width, height = extension_capture_sizes[0]
184        accepted_format = format_name
185        break
186      else:
187        raise AssertionError('No supported sizes/formats found!')
188
189      # Set tablet brightness to darken scene
190      self.set_screen_brightness(_TABLET_BRIGHTNESS)
191
192      file_stem = f'{test_name}_{self.camera_id}_{accepted_format}_{width}x{height}'
193      out_surfaces = {
194          'format': accepted_format, 'width': width, 'height': height}
195      req = capture_request_utils.auto_capture_request()
196
197      logging.debug('Taking auto capture with night mode ON')
198      night_cap = self._take_capture(
199          cam, req, out_surfaces)
200      rgb_night_img = _convert_capture(night_cap, f'{file_stem}_night')
201
202      # Assert correct behavior and create luminosity plots
203      low_light_utils.analyze_low_light_scene_capture(
204          f'{file_stem}_night',
205          cv2.cvtColor(rgb_night_img, cv2.COLOR_RGB2BGR),
206          _AVG_LUMINANCE_THRESH,
207          _AVG_DELTA_LUMINANCE_THRESH
208      )
209
210if __name__ == '__main__':
211  test_runner.main()
212