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 that the LED snapshot works correctly."""
15
16import logging
17import os.path
18
19import camera_properties_utils
20import capture_request_utils
21import image_processing_utils
22import its_base_test
23import its_session_utils
24import lighting_control_utils
25from mobly import test_runner
26
27_AE_MODES = {0: 'OFF', 1: 'ON', 2: 'ON_AUTO_FLASH', 3: 'ON_ALWAYS_FLASH',
28             4: 'ON_AUTO_FLASH_REDEYE', 5: 'ON_EXTERNAL_FLASH'}
29_AE_STATES = {0: 'INACTIVE', 1: 'SEARCHING', 2: 'CONVERGED', 3: 'LOCKED',
30              4: 'FLASH_REQUIRED', 5: 'PRECAPTURE'}
31_FLASH_STATES = {0: 'FLASH_STATE_UNAVAILABLE', 1: 'FLASH_STATE_CHARGING',
32                 2: 'FLASH_STATE_READY', 3: 'FLASH_STATE_FIRED',
33                 4: 'FLASH_STATE_PARTIAL'}
34_FORMAT_NAMES = ('jpeg', 'yuv')
35_IMG_SIZES = ((640, 480), (640, 360))
36_VGA_SIZE = (640, 480)
37_CH_FULL_SCALE = 255
38_TEST_NAME = os.path.splitext(os.path.basename(__file__))[0]
39_AE_MODE_ON_AUTO_FLASH = 2
40_CAPTURE_INTENT_PREVIEW = 1
41_CAPTURE_INTENT_STILL_CAPTURE = 2
42_AE_PRECAPTURE_TRIGGER_START = 1
43_AE_PRECAPTURE_TRIGGER_IDLE = 0
44_FLASH_MEAN_MIN = 50
45_FLASH_MEAN_MAX = 200
46_WB_MIN = 0.8
47_WB_MAX = 1.2
48_COLOR_CHANNELS = ('R', 'G', 'B')
49
50
51def _take_captures(out_surfaces, cam, img_name, flash=False):
52  """Takes captures and returns the captured image.
53
54  Args:
55    out_surfaces:
56    cam: ItsSession util object
57    img_name: image name to be saved.
58    flash: True if the capture needs to be taken with Flash ON
59
60  Returns:
61    cap: captured image object as defined by
62    ItsSessionUtils.do_capture()
63  """
64  cam.do_3a(do_af=False)
65  if not flash:
66    cap_req = capture_request_utils.auto_capture_request()
67    cap_req[
68        'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
69    cap = cam.do_capture(cap_req, out_surfaces)
70  else:
71    cap = capture_request_utils.take_captures_with_flash(cam, out_surfaces)
72
73  img = image_processing_utils.convert_capture_to_rgb_image(cap)
74  # Save captured image
75  image_processing_utils.write_image(img, img_name)
76  return cap
77
78
79def _is_color_mean_valid(means, color_channel, fmt_name, width, height):
80  """Checks if the mean for color_channel is within the range.
81
82  Computes means for color_channel specified and checks whether
83  it is within the acceptable range.
84  Args:
85    means: list of means in float
86    color_channel: String; values must be one of the color
87      channels in _COLOR_CHANNELS
88    fmt_name: Format to be tested
89    width: width of the image to be tested
90    height: height of the image to be tested
91
92  Returns:
93    True if the color mean is within the range and returns False
94    if invalid.
95  """
96  if color_channel not in _COLOR_CHANNELS:
97    raise AssertionError('Invalid color_channel.')
98
99  if color_channel == 'R':
100    color_mean = means[0]
101  elif color_channel == 'G':
102    color_mean = means[1]
103  else:
104    color_mean = means[2]
105
106  if not _FLASH_MEAN_MIN <= color_mean <= _FLASH_MEAN_MAX:
107    logging.debug('Flash image mean %s not'
108                  ' within limits for channel %s.'
109                  ' Format: %s,'
110                  ' Size: %sx%s', color_mean, color_channel,
111                  fmt_name, width, height)
112    return False
113  else:
114    return True
115
116
117class LedSnapshotTest(its_base_test.ItsBaseTest):
118  """Tests if LED snapshot works correctly.
119
120  In this test we capture the failure that the LED snapshot is not too dark,
121  too bright or producing a strange color tint.
122
123  During the test 3 images are captured for each format in _FORMAT_NAMES
124  and size in _IMG_SIZES:
125  1. Lights ON, AUTO_FLASH set to OFF -> Baseline capture without any flash.
126  2. Lights OFF, AUTO_FLASH set to OFF -> Ensures dark lighting conditions
127     to trigger the flash.
128  3. Lights OFF, AUTO_FLASH set to ON -> Still capture with flash
129
130  For all the 3 pictures we compute the image means and log them.
131  For the capture with flash triggered, we compare the mean to be within the
132  minimum and maximum threshold level. The capture with flash should not be too
133  dark or too bright.
134  In order to ensure the white balance, the ratio of R/G and B/G is also
135  compared to be within the pre-decided threshold level.
136  Failures will be reported if any of the measuremenet is out of range.
137  """
138
139  def test_led_snapshot(self):
140    test_name = os.path.join(self.log_path, _TEST_NAME)
141
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
149      # check SKIP conditions
150      first_api_level = its_session_utils.get_first_api_level(
151          self.dut.serial)
152      camera_properties_utils.skip_unless(
153          camera_properties_utils.flash(props) and
154          first_api_level >= its_session_utils.ANDROID14_API_LEVEL)
155      failure_messages = []
156      # establish connection with lighting controller
157      arduino_serial_port = lighting_control_utils.lighting_control(
158          self.lighting_cntl, self.lighting_ch)
159      for fmt_name in _FORMAT_NAMES:
160        for size in _IMG_SIZES:
161          width, height = size
162          if not (fmt_name == 'yuv' and size == _VGA_SIZE):
163            output_sizes = capture_request_utils.get_available_output_sizes(
164                fmt_name, props, match_ar_size=size)
165            if not output_sizes:
166              if size != _VGA_SIZE:
167                logging.debug('No output sizes for format %s, size %sx%s',
168                              fmt_name, width, height)
169                continue
170              else:
171                raise AssertionError(f'No output sizes for format {fmt_name}, '
172                                     f'size {width}x{height}')
173            # pick smallest size out of available output sizes
174            width, height = output_sizes[-1]
175
176          out_surfaces = {'format': fmt_name, 'width': width, 'height': height}
177          logging.debug(
178              'Testing %s format, size: %dx%d', fmt_name, width, height)
179
180          # take capture with lights on - no flash
181          logging.debug(
182              'Taking reference frame with lights on and no flash.')
183          img_prefix = f'{test_name}_{fmt_name}_{width}x{height}'
184          light_on_img_name = f'{img_prefix}_lights_on.jpg'
185          _take_captures(out_surfaces, cam, light_on_img_name, flash=False)
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          # take capture with no flash as baseline
192          logging.debug(
193              'Taking reference frame with lights off and no auto-flash.')
194          no_flash_req = capture_request_utils.auto_capture_request()
195          no_flash_req[
196              'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
197          no_flash_img_name = f'{img_prefix}_no_flash.jpg'
198          _take_captures(out_surfaces, cam, no_flash_img_name, flash=False)
199
200          # take capture with auto flash enabled
201          logging.debug('Taking capture with auto flash enabled.')
202          flash_fired = False
203          flash_img_name = f'{img_prefix}_flash.jpg'
204          cap = _take_captures(out_surfaces, cam, flash_img_name, flash=True)
205          img = image_processing_utils.convert_capture_to_rgb_image(cap)
206
207          # evaluate captured image
208          metadata = cap['metadata']
209          exp = int(metadata['android.sensor.exposureTime'])
210          iso = int(metadata['android.sensor.sensitivity'])
211          logging.debug('cap ISO: %d, exp: %d ns', iso, exp)
212          logging.debug('AE_MODE (cap): %s',
213                        _AE_MODES[metadata['android.control.aeMode']])
214          ae_state = _AE_STATES[metadata['android.control.aeState']]
215          logging.debug('AE_STATE (cap): %s', ae_state)
216          flash_state = _FLASH_STATES[metadata['android.flash.state']]
217          logging.debug('FLASH_STATE: %s', flash_state)
218          if flash_state == 'FLASH_STATE_FIRED':
219            logging.debug('Flash fired')
220            flash_fired = True
221            flash_means = image_processing_utils.compute_image_means(img)
222            logging.debug('Image means with flash: %s', flash_means)
223            flash_means = [i * _CH_FULL_SCALE for i in flash_means]
224            logging.debug('Flash capture rgb means: %s', flash_means)
225
226            # Verify that R/G and B/G ratios are within the limits
227            r_g_ratio = flash_means[0]/ flash_means[1]
228            logging.debug('R/G ratio: %s fmt: %s, WxH: %sx%s',
229                          r_g_ratio, fmt_name, width, height)
230            b_g_ratio = flash_means[2]/flash_means[1]
231            logging.debug('B/G ratio: %s fmt: %s, WxH: %sx%s',
232                          b_g_ratio, fmt_name, width, height)
233
234            if not _WB_MIN <= r_g_ratio <= _WB_MAX:
235              failure_messages.append(f'R/G ratio: {r_g_ratio} not within'
236                                      f' the limits. Format: {fmt_name},'
237                                      f' Size: {width}x{height}')
238            if not _WB_MIN <= b_g_ratio <= _WB_MAX:
239              failure_messages.append(f'B/G ratio: {r_g_ratio} not within'
240                                      f' the limits. Format: {fmt_name},'
241                                      f' Size: {width}x{height}')
242
243            # Check whether the image means for each color channel is
244            # within the limits or not.
245            valid_color = True
246            for color in _COLOR_CHANNELS:
247              valid_color = _is_color_mean_valid(flash_means, color,
248                                                 fmt_name, width, height)
249              if not valid_color:
250                failure_messages.append(
251                    f'Flash image mean not within limits for channel {color}.'
252                    f' Format: {fmt_name},Size: {width}x{height}')
253
254          if not flash_fired:
255            raise AssertionError(
256                'Flash was not fired. Format:{fmt_name}, Size:{width}x{height}')
257
258          # turn the lights back on
259          lighting_control_utils.set_lighting_state(
260              arduino_serial_port, self.lighting_ch, 'ON')
261
262      # assert correct behavior for all formats
263      if failure_messages:
264        raise AssertionError('\n'.join(failure_messages))
265
266if __name__ == '__main__':
267  test_runner.main()
268