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 flash strength control in TORCH mode works correctly during camera use."""
15
16import logging
17import os.path
18
19from mobly import test_runner
20import numpy as np
21import its_base_test
22import camera_properties_utils
23import capture_request_utils
24import image_processing_utils
25import its_session_utils
26import lighting_control_utils
27
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_BRIGHTNESS_MEAN_ATOL = 5  # Tolerance for brightness mean
34_BURST_LEN = 5
35_CAPTURE_INTENT_PREVIEW = 1
36_CAPTURE_INTENT_STILL_CAPTURE = 2
37_FLASH_STATES = {0: 'FLASH_STATE_UNAVAILABLE', 1: 'FLASH_STATE_CHARGING',
38                 2: 'FLASH_STATE_READY', 3: 'FLASH_STATE_FIRED',
39                 4: 'FLASH_STATE_PARTIAL'}
40_FORMAT_NAME = 'yuv'
41_IMG_SIZE = (640, 360)
42_MAX_SINGLE_STRENGTH_PROP_KEY = 'android.flash.singleStrengthMaxLevel'
43_MAX_TORCH_STRENGTH_PROP_KEY = 'android.flash.torchStrengthMaxLevel'
44_PATCH_H = 0.25  # center 25%
45_PATCH_W = 0.25
46_PATCH_X = 0.5-_PATCH_W/2
47_PATCH_Y = 0.5-_PATCH_H/2
48_SINGLE_STRENGTH_CONTROL_THRESHOLD = 1
49_STRENGTH_STEPS = 3  # Steps of flash strengths to be tested
50_TEST_NAME = os.path.splitext(os.path.basename(__file__))[0]
51_TESTING_AE_MODES = (0, 1, 2)
52_TORCH_MODE = 2
53_TORCH_STRENGTH_CONTROL_THRESHOLD = 1
54_TORCH_STRENGTH_MIN = 0
55
56
57# TODO: b/344675052 - Add torch strength control in do_3a()
58def _take_captures(
59    self, arduino_serial_port, out_surfaces, cam,
60    img_name_prefix, ae_mode, torch_strength
61):
62  """Takes video captures and returns the captured images.
63
64  Args:
65    self: ItsBaseTest object; used for lighting control.
66    arduino_serial_port: serial port pointer; used for lighting control
67    out_surfaces: list; valid output surfaces for caps.
68    cam: ItsSession util object.
69    img_name_prefix: image name to be saved, log_path included.
70    ae_mode: AE mode to be tested with.
71    torch_strength: Flash strength that flash should be fired with.
72      Note that 0 is for baseline capture.
73
74  Returns:
75    caps: list of capture objects as described by cam.do_capture().
76  """
77  # Take base image without flash
78  if torch_strength == 0:
79    # turn OFF lights to darken scene
80    lighting_control_utils.set_lighting_state(
81        arduino_serial_port, self.lighting_ch, 'OFF'
82    )
83    cam.do_3a(do_af=False, lock_awb=True)
84    cap_req = capture_request_utils.auto_capture_request()
85    cap_req[
86        'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
87    cap_req['android.control.aeMode'] = 0  # AE_MODE_OFF
88    cap_req['android.control.awbLock'] = True
89    cap = cam.do_capture(cap_req, out_surfaces)
90    # turn the lights back on
91    lighting_control_utils.set_lighting_state(
92        arduino_serial_port, self.lighting_ch, 'ON'
93    )
94    return [cap]
95
96  # Take multiple still captures with torch strength
97  else:
98    cam.do_3a(do_af=False, lock_awb=True)
99    # turn OFF lights to darken scene
100    lighting_control_utils.set_lighting_state(
101        arduino_serial_port, self.lighting_ch, 'OFF'
102    )
103    cap_req = capture_request_utils.auto_capture_request()
104    cap_req['android.control.aeMode'] = ae_mode
105    cap_req['android.control.captureIntent'] = _CAPTURE_INTENT_PREVIEW
106    cap_req['android.control.aeLock'] = True
107    cap_req['android.control.awbLock'] = True  # AWB Lock
108    cap_req['android.flash.mode'] = _TORCH_MODE
109    cap_req['android.flash.strengthLevel'] = torch_strength
110    reqs = [cap_req] * _BURST_LEN
111    caps = cam.do_capture(reqs, out_surfaces)
112    # turn the lights back on
113    lighting_control_utils.set_lighting_state(
114        arduino_serial_port, self.lighting_ch, 'ON'
115    )
116    for i, cap in enumerate(caps):
117      img = image_processing_utils.convert_capture_to_rgb_image(cap)
118      # Save captured image
119      image_processing_utils.write_image(img, f'{img_name_prefix}{i}.jpg')
120    return caps
121
122
123def _get_img_patch_mean(caps, props):
124  """Evaluate captured image by extracting means in the center patch.
125
126  Args:
127    caps: captured list of image object as defined by
128      ItsSessionUtils.do_capture().
129    props: Camera properties object.
130
131  Returns:
132    mean: (list of float64) calculated means of Y plane center patch.
133  """
134  flash_means = []
135  for cap in caps:
136    metadata = cap['metadata']
137    exp = int(metadata['android.sensor.exposureTime'])
138    iso = int(metadata['android.sensor.sensitivity'])
139    flash_exp_x_iso = []
140    logging.debug('cap ISO: %d, exp: %d ns', iso, exp)
141    logging.debug('AE_MODE (cap): %s',
142                  _AE_MODES[metadata['android.control.aeMode']])
143    ae_state = _AE_STATES[metadata['android.control.aeState']]
144    logging.debug('AE_STATE (cap): %s', ae_state)
145    flash_state = _FLASH_STATES[metadata['android.flash.state']]
146    logging.debug('FLASH_STATE: %s', flash_state)
147    logging.debug('FLASH_STRENGTH: %s', metadata['android.flash.strengthLevel'])
148
149    flash_exp_x_iso = exp*iso
150    y, _, _ = image_processing_utils.convert_capture_to_planes(
151        cap, props)
152    patch = image_processing_utils.get_image_patch(
153        y, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
154    flash_mean = image_processing_utils.compute_image_means(
155        patch)[0]*255
156    flash_means.append(flash_mean)
157
158    logging.debug('Flash exposure X ISO %d', flash_exp_x_iso)
159    logging.debug('Flash frames Y mean: %.4f', flash_mean)
160  return flash_means
161
162
163def _compare_means(formats_means, ae_mode, flash_strengths):
164  """Compares the means of the captured images at different strength levels.
165
166  If AE_MODE is ON/OFF, capture should show mean differences
167  in flash strengths. If AE_MODE is ON_AUTO_FLASH, flash
168  strength should be overwritten hence no mean difference in captures.
169
170  Args:
171    formats_means: list of calculated means of image center patches of all req.
172    ae_mode: requested AE mode during testing.
173    flash_strengths: list of flash strength values requested during testing.
174
175  Returns:
176    failure_messages: (list of string) list of error messages.
177  """
178
179  failure_messages = []
180  strength_means = [np.average(x) for x in formats_means]
181  # Intentionally omitting frame-to-frame sameness check of last burst
182  for i, burst_means in enumerate(formats_means[:-1]):
183    # Check for strength brightness with averages of same strength captures
184    if (strength_means[i] >= strength_means[i+1] and
185        ae_mode in _AE_MODE_FLASH_CONTROL):
186      msg = (
187          f'Capture with AE_CONTROL_MODE OFF/ON. AE_MODE: {ae_mode}; '
188          f'Strength {flash_strengths[i]} mean: {strength_means[i]}; '
189          f'Strength {flash_strengths[i+1]} mean: {strength_means[i+1]}; '
190          f'Mean of {flash_strengths[i+1]} should be brighter than '
191          f'Mean of {flash_strengths[i]}!'
192      )
193      failure_messages.append(msg)
194    for j in range(len(burst_means)-1):
195      # Check for frame-to-frame sameness
196      diff = abs(burst_means[j] - burst_means[j+1])
197      if diff > _BRIGHTNESS_MEAN_ATOL:
198        if ae_mode in _AE_MODE_FLASH_CONTROL:
199          msg = (
200              f'Capture with AE_CONTROL_MODE OFF/ON. AE_MODE: {ae_mode}; '
201              f'Strength {flash_strengths[i]} capture {j} mean: '
202              f'{burst_means[j]},'
203              f'Strength {flash_strengths[i+1]} capture {j+1} mean: '
204              f'{burst_means[j+1]}, '
205              f'Torch strength is not consistent between captures '
206              f'Diff: {diff}; TOL: {_BRIGHTNESS_MEAN_ATOL}'
207          )
208        else:
209          msg = (
210              f'Capture with AE_CONTROL_MODE ON_AUTO_FLASH. '
211              f'Strength {flash_strengths[i]} mean: {burst_means[j]}, '
212              f'Strength {flash_strengths[i+1]} mean: '
213              f'{burst_means[j+1]}. '
214              f'Diff: {diff}; TOL: {_BRIGHTNESS_MEAN_ATOL}'
215          )
216        failure_messages.append(msg)
217
218  return failure_messages
219
220
221class TorchStrengthTest(its_base_test.ItsBaseTest):
222  """Test if torch strength control feature works as intended."""
223
224  def test_torch_strength(self):
225    name_with_path = os.path.join(self.log_path, _TEST_NAME)
226
227    with its_session_utils.ItsSession(
228        device_id=self.dut.serial,
229        camera_id=self.camera_id,
230        hidden_physical_id=self.hidden_physical_id) as cam:
231      props = cam.get_camera_properties()
232      props = cam.override_with_hidden_physical_camera_props(props)
233
234      # check SKIP conditions
235      max_flash_strength = props[_MAX_SINGLE_STRENGTH_PROP_KEY]
236      max_torch_strength = props[_MAX_TORCH_STRENGTH_PROP_KEY]
237      camera_properties_utils.skip_unless(
238          camera_properties_utils.flash(props) and
239          max_flash_strength > _SINGLE_STRENGTH_CONTROL_THRESHOLD and
240          max_torch_strength > _TORCH_STRENGTH_CONTROL_THRESHOLD
241      )
242
243      # establish connection with lighting controller
244      arduino_serial_port = lighting_control_utils.lighting_control(
245          self.lighting_cntl, self.lighting_ch
246      )
247
248      failure_messages = []
249      # testing at 80% of max strength
250      max_torch_strength = max_torch_strength * 0.8
251      # list with no torch (baseline), linear strength steps, 0.8 max strength
252      torch_strengths = [max_torch_strength*i/_STRENGTH_STEPS for i in
253                         range(_STRENGTH_STEPS)]
254      torch_strengths.append(max_torch_strength)
255      logging.debug('Testing flash strengths: %s', torch_strengths)
256      for ae_mode in _TESTING_AE_MODES:
257        formats_means = []
258        for strength in torch_strengths:
259          if (_TORCH_STRENGTH_MIN < strength <=
260              _TORCH_STRENGTH_CONTROL_THRESHOLD):
261            logging.debug('Torch strength value <= %d, test case ignored',
262                          _TORCH_STRENGTH_CONTROL_THRESHOLD)
263          else:
264            # naming images to be captured
265            img_name_prefix = (
266                f'{name_with_path}_ae_mode={ae_mode}_'
267                f'torch_strength={strength}_'
268            )
269            # defining out_surfaces
270            width, height = _IMG_SIZE
271            out_surfaces = {'format': _FORMAT_NAME,
272                            'width': width, 'height': height}
273            # take capture and evaluate
274            caps = _take_captures(
275                self, arduino_serial_port, out_surfaces, cam,
276                img_name_prefix, ae_mode, strength
277            )
278            formats_means.append(_get_img_patch_mean(caps, props))
279
280        # Compare means and compose failure messages
281        failure_messages += _compare_means(formats_means,
282                                           ae_mode, torch_strengths)
283
284    # turn the lights back on
285    lighting_control_utils.set_lighting_state(
286        arduino_serial_port, self.lighting_ch, 'ON')
287
288    # assert correct behavior and print error message(s)
289    if failure_messages:
290      raise AssertionError('\n'.join(failure_messages))
291
292if __name__ == '__main__':
293  test_runner.main()
294
295