• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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(device_id=self.dut.serial,
143        camera_id=self.camera_id,
144        hidden_physical_id=self.hidden_physical_id) as cam:
145      props = cam.get_camera_properties()
146      props = cam.override_with_hidden_physical_camera_props(props)
147
148      # check SKIP conditions
149      vendor_api_level = its_session_utils.get_vendor_api_level(
150          self.dut.serial)
151      camera_properties_utils.skip_unless(
152          camera_properties_utils.flash(props) and
153          vendor_api_level >= its_session_utils.ANDROID14_API_LEVEL)
154      failure_messages = []
155      # establish connection with lighting controller
156      arduino_serial_port = lighting_control_utils.lighting_control(
157          self.lighting_cntl, self.lighting_ch)
158      for fmt_name in _FORMAT_NAMES:
159        for size in _IMG_SIZES:
160          width, height = size
161          if not (fmt_name == 'yuv' and size == _VGA_SIZE):
162            output_sizes = capture_request_utils.get_available_output_sizes(
163                fmt_name, props, match_ar_size=size)
164            if not output_sizes:
165              if size != _VGA_SIZE:
166                logging.debug('No output sizes for format %s, size %sx%s',
167                              fmt_name, width, height)
168                continue
169              else:
170                raise AssertionError(f'No output sizes for format {fmt_name}, '
171                                     f'size {width}x{height}')
172            # pick smallest size out of available output sizes
173            width, height = output_sizes[-1]
174
175          out_surfaces = {'format': fmt_name, 'width': width, 'height': height}
176          logging.debug(
177              'Testing %s format, size: %dx%d', fmt_name, width, height)
178
179          # take capture with lights on - no flash
180          logging.debug(
181              'Taking reference frame with lights on and no flash.')
182          img_prefix = f'{test_name}_{fmt_name}_{width}x{height}'
183          light_on_img_name = f'{img_prefix}_lights_on.jpg'
184          _take_captures(out_surfaces, cam, light_on_img_name, flash=False)
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          # take capture with no flash as baseline
191          logging.debug(
192              'Taking reference frame with lights off and no auto-flash.')
193          no_flash_req = capture_request_utils.auto_capture_request()
194          no_flash_req[
195              'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
196          no_flash_img_name = f'{img_prefix}_no_flash.jpg'
197          _take_captures(out_surfaces, cam, no_flash_img_name, flash=False)
198
199          # take capture with auto flash enabled
200          logging.debug('Taking capture with auto flash enabled.')
201          flash_fired = False
202          flash_img_name = f'{img_prefix}_flash.jpg'
203          cap = _take_captures(out_surfaces, cam, flash_img_name, flash=True)
204          img = image_processing_utils.convert_capture_to_rgb_image(cap)
205
206          # evaluate captured image
207          metadata = cap['metadata']
208          exp = int(metadata['android.sensor.exposureTime'])
209          iso = int(metadata['android.sensor.sensitivity'])
210          logging.debug('cap ISO: %d, exp: %d ns', iso, exp)
211          logging.debug('AE_MODE (cap): %s',
212                        _AE_MODES[metadata['android.control.aeMode']])
213          ae_state = _AE_STATES[metadata['android.control.aeState']]
214          logging.debug('AE_STATE (cap): %s', ae_state)
215          flash_state = _FLASH_STATES[metadata['android.flash.state']]
216          logging.debug('FLASH_STATE: %s', flash_state)
217          if flash_state == 'FLASH_STATE_FIRED':
218            logging.debug('Flash fired')
219            flash_fired = True
220            flash_means = image_processing_utils.compute_image_means(img)
221            logging.debug('Image means with flash: %s', flash_means)
222            flash_means = [i * _CH_FULL_SCALE for i in flash_means]
223            logging.debug('Flash capture rgb means: %s', flash_means)
224
225            # Verify that R/G and B/G ratios are within the limits
226            r_g_ratio = flash_means[0]/ flash_means[1]
227            logging.debug('R/G ratio: %s fmt: %s, WxH: %sx%s',
228                          r_g_ratio, fmt_name, width, height)
229            b_g_ratio = flash_means[2]/flash_means[1]
230            logging.debug('B/G ratio: %s fmt: %s, WxH: %sx%s',
231                          b_g_ratio, fmt_name, width, height)
232
233            if not _WB_MIN <= r_g_ratio <= _WB_MAX:
234              failure_messages.append(f'R/G ratio: {r_g_ratio} not within'
235                                      f' the limits. Format: {fmt_name},'
236                                      f' Size: {width}x{height}')
237            if not _WB_MIN <= b_g_ratio <= _WB_MAX:
238              failure_messages.append(f'B/G ratio: {r_g_ratio} not within'
239                                      f' the limits. Format: {fmt_name},'
240                                      f' Size: {width}x{height}')
241
242            # Check whether the image means for each color channel is
243            # within the limits or not.
244            valid_color = True
245            for color in _COLOR_CHANNELS:
246              valid_color = _is_color_mean_valid(flash_means, color,
247                                                 fmt_name, width, height)
248              if not valid_color:
249                failure_messages.append(
250                    f'Flash image mean not within limits for channel {color}.'
251                    f' Format: {fmt_name},Size: {width}x{height}')
252
253          if not flash_fired:
254            raise AssertionError(
255                'Flash was not fired. Format:{fmt_name}, Size:{width}x{height}')
256
257          # turn the lights back on
258          lighting_control_utils.set_lighting_state(
259              arduino_serial_port, self.lighting_ch, 'ON')
260
261      # assert correct behavior for all formats
262      if failure_messages:
263        raise AssertionError('\n'.join(failure_messages))
264
265if __name__ == '__main__':
266  test_runner.main()
267