xref: /aosp_15_r20/cts/apps/CameraITS/tests/scene_flash/test_flash_strength.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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 manual flash strength control (SINGLE capture mode) works correctly."""
15
16import logging
17import os.path
18
19from mobly import test_runner
20import camera_properties_utils
21import capture_request_utils
22import image_processing_utils
23import its_base_test
24import its_session_utils
25import lighting_control_utils
26
27_TESTING_AE_MODES = (0, 1, 2)
28_AE_MODE_FLASH_CONTROL = (0, 1)
29_AE_MODES = {0: 'OFF', 1: 'ON', 2: 'ON_AUTO_FLASH', 3: 'ON_ALWAYS_FLASH',
30             4: 'ON_AUTO_FLASH_REDEYE', 5: 'ON_EXTERNAL_FLASH'}
31_AE_STATES = {0: 'INACTIVE', 1: 'SEARCHING', 2: 'CONVERGED', 3: 'LOCKED',
32              4: 'FLASH_REQUIRED', 5: 'PRECAPTURE'}
33_FLASH_STATES = {0: 'FLASH_STATE_UNAVAILABLE', 1: 'FLASH_STATE_CHARGING',
34                 2: 'FLASH_STATE_READY', 3: 'FLASH_STATE_FIRED',
35                 4: 'FLASH_STATE_PARTIAL'}
36_FORMAT_NAME = 'yuv'
37_IMG_SIZE = (640, 480)
38_PATCH_H = 0.5  # center 50%
39_PATCH_W = 0.5
40_PATCH_X = 0.5-_PATCH_W/2
41_PATCH_Y = 0.5-_PATCH_H/2
42_TEST_NAME = os.path.splitext(os.path.basename(__file__))[0]
43_CAPTURE_INTENT_STILL_CAPTURE = 2
44_MAX_FLASH_STRENGTH = 'android.flash.singleStrengthMaxLevel'
45_MAX_TORCH_STRENGTH = 'android.flash.torchStrengthMaxLevel'
46_BRIGHTNESS_MEAN_ATOL = 15  # Tolerance for brightness mean
47_STRENGTH_STEPS = 3  # Steps of flash strengths to be tested
48
49
50def _take_captures(out_surfaces, cam, img_name, ae_mode, strength=0):
51  """Takes captures and returns the captured image.
52
53  Args:
54    out_surfaces: list; valid output surfaces for caps.
55    cam: ItsSession util object.
56    img_name: image name to be saved.
57    ae_mode: AE mode to be tested with.
58    strength: Flash strength that flash should be fired with.
59      Note that 0 is for baseline capture.
60
61  Returns:
62    cap: captured image object as defined by
63      ItsSessionUtils.do_capture().
64  """
65  cam.do_3a(do_af=False)
66  # Take base image without flash
67  if strength == 0:
68    cap_req = capture_request_utils.auto_capture_request()
69    cap_req[
70        'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
71    cap_req['android.control.aeMode'] = 0
72    cap = cam.do_capture(cap_req, out_surfaces)
73    logging.debug('Capturing base image without flash')
74  # Take capture with flash strength
75  else:
76    cap = capture_request_utils.take_captures_with_flash_strength(
77        cam, out_surfaces, ae_mode, strength)
78    logging.debug('Capturing image with flash strength: %s', strength)
79  img = image_processing_utils.convert_capture_to_rgb_image(cap)
80  # Save captured image
81  image_processing_utils.write_image(img, img_name)
82  return cap
83
84
85def _get_mean(cap, props):
86  """Evaluate captured image by extracting means in the center patch.
87
88  Args:
89    cap: captured image object as defined by
90      ItsSessionUtils.do_capture().
91    props: Camera properties object.
92
93  Returns:
94    mean: (float64) calculated mean of image center patch.
95  """
96  metadata = cap['metadata']
97  exp = int(metadata['android.sensor.exposureTime'])
98  iso = int(metadata['android.sensor.sensitivity'])
99  flash_exp_x_iso = []
100  logging.debug('cap ISO: %d, exp: %d ns', iso, exp)
101  logging.debug('AE_MODE (cap): %s',
102                _AE_MODES[metadata['android.control.aeMode']])
103  ae_state = _AE_STATES[metadata['android.control.aeState']]
104  logging.debug('AE_STATE (cap): %s', ae_state)
105  flash_state = _FLASH_STATES[metadata['android.flash.state']]
106  logging.debug('FLASH_STATE: %s', flash_state)
107
108  flash_exp_x_iso = exp*iso
109  y, _, _ = image_processing_utils.convert_capture_to_planes(
110      cap, props)
111  patch = image_processing_utils.get_image_patch(
112      y, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
113  flash_mean = image_processing_utils.compute_image_means(
114      patch)[0]*255
115  flash_grad = image_processing_utils.compute_image_max_gradients(
116      patch)[0]*255
117
118  # log results
119  logging.debug('Flash exposure X ISO %d', flash_exp_x_iso)
120  logging.debug('Flash frames Y grad: %.4f', flash_grad)
121  logging.debug('Flash frames Y mean: %.4f', flash_mean)
122  return flash_mean
123
124
125def _compare_means(formats_means, ae_mode, flash_strengths):
126  """Analyzes test results and generates failure messages.
127
128  If AE_MODE is ON/OFF, capture should show mean differences
129  in flash strengths. If AE_MODE is ON_AUTO_FLASH, flash
130  strength should be overwritten hence no mean difference in captures.
131
132  Args:
133    formats_means: list of calculated means of image center patches.
134    ae_mode: requested AE mode during testing.
135    flash_strengths: list of flash strength values requested during testing.
136
137  Returns:
138    failure_messages: (list of string) list of error messages.
139  """
140  failure_messages = []
141  if ae_mode in _AE_MODE_FLASH_CONTROL:
142    for mean in range(1, len(formats_means)-1):
143      if formats_means[mean] >= formats_means[mean+1]:
144        msg = (
145            f'Capture with CONTROL_AE_MODE {_AE_MODES[ae_mode]}. '
146            f'Strength {flash_strengths[mean]} mean: {formats_means[mean]}; '
147            f'Strength {flash_strengths[mean+1]} mean: '
148            f'{formats_means[mean+1]}. '
149            f'Mean of {flash_strengths[mean+1]} should be brighter than '
150            f'Mean of {flash_strengths[mean]}. '
151        )
152        failure_messages.append(msg)
153  else:
154    for mean in range(1, len(formats_means)-1):
155      diff = abs(formats_means[mean] - formats_means[mean+1])
156      if diff > _BRIGHTNESS_MEAN_ATOL:
157        msg = (
158            f'Capture with CONTROL_AE_MODE {_AE_MODES[ae_mode]}. '
159            f'Strength {flash_strengths[mean]} mean: {formats_means[mean]}; '
160            f'Strength {flash_strengths[mean+1]} mean: '
161            f'{formats_means[mean+1]}. '
162            f'Diff: {diff}; ATOL: {_BRIGHTNESS_MEAN_ATOL}. '
163        )
164        failure_messages.append(msg)
165  return failure_messages
166
167
168class FlashStrengthTest(its_base_test.ItsBaseTest):
169  """Test if flash strength control (SINGLE capture mode) feature works as intended."""
170
171  def test_flash_strength(self):
172    name_with_path = os.path.join(self.log_path, _TEST_NAME)
173
174    with its_session_utils.ItsSession(
175        device_id=self.dut.serial,
176        camera_id=self.camera_id,
177        hidden_physical_id=self.hidden_physical_id) as cam:
178      props = cam.get_camera_properties()
179      props = cam.override_with_hidden_physical_camera_props(props)
180
181      # check SKIP conditions
182      max_flash_strength = props[_MAX_FLASH_STRENGTH]
183      max_torch_strength = props[_MAX_TORCH_STRENGTH]
184      camera_properties_utils.skip_unless(
185          camera_properties_utils.flash(props) and
186          max_flash_strength > 1 and max_torch_strength > 1)
187      # establish connection with lighting controller
188      arduino_serial_port = lighting_control_utils.lighting_control(
189          self.lighting_cntl, self.lighting_ch)
190
191      # turn OFF lights to darken scene
192      lighting_control_utils.set_lighting_state(
193          arduino_serial_port, self.lighting_ch, 'OFF')
194
195      failure_messages = []
196      # list with no flash (baseline), linear strength steps, max strength
197      flash_strengths = [max_flash_strength*i/_STRENGTH_STEPS for i in
198                         range(_STRENGTH_STEPS)]
199      flash_strengths.append(max_flash_strength)
200      logging.debug('Testing flash strengths: %s', flash_strengths)
201      # loop through ae modes to be tested
202      for ae_mode in _TESTING_AE_MODES:
203        formats_means = []
204        # loop through flash strengths
205        for strength in flash_strengths:
206          if 0 < strength <= 1:
207            logging.debug('Flash strength value <=1, test case ignored')
208          else:
209            # naming images to be captured
210            img_name = f'{name_with_path}_ae_mode={ae_mode}_flash_strength={strength}.jpg'
211            # check if testing image size is supported, if not use mid size
212            output_sizes = capture_request_utils.get_available_output_sizes(
213                _FORMAT_NAME, props)
214            if _IMG_SIZE in output_sizes:
215              width, height = _IMG_SIZE
216              logging.debug(
217                  'Testing with default image size: %dx%d', width, height
218              )
219            else:
220              width, height = output_sizes[len(output_sizes)//2]
221              logging.debug(
222                  'Default size not supported, testing with size: %dx%d',
223                  width, height
224              )
225            # defining out_surfaces
226            out_surfaces = {'format': _FORMAT_NAME,
227                            'width': width, 'height': height}
228            # take capture and evaluate
229            cap = _take_captures(out_surfaces, cam, img_name, ae_mode, strength)
230            formats_means.append(_get_mean(cap, props))
231        check_mean = True
232        first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
233        if (
234            ae_mode == 1 and
235            first_api_level <= its_session_utils.ANDROID15_API_LEVEL
236        ):
237          check_mean = False
238        # Compare means and assert PASS/FAIL
239        if check_mean:
240          failure_messages += _compare_means(formats_means,
241                                             ae_mode, flash_strengths)
242
243    # turn the lights back on
244    lighting_control_utils.set_lighting_state(
245        arduino_serial_port, self.lighting_ch, 'ON')
246    if failure_messages:
247      raise AssertionError('\n'.join(failure_messages))
248
249if __name__ == '__main__':
250  test_runner.main()
251