blob: 6b06c7fe4950be42d02f8e73b83373356454b36d [file] [log] [blame]
# Copyright 2024 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Verify flash strength control in TORCH mode works correctly during camera use."""
import logging
import os.path
from mobly import test_runner
import numpy as np
import its_base_test
import camera_properties_utils
import capture_request_utils
import image_processing_utils
import its_session_utils
import lighting_control_utils
_AE_MODE_FLASH_CONTROL = (0, 1)
_AE_MODES = {0: 'OFF', 1: 'ON', 2: 'ON_AUTO_FLASH', 3: 'ON_ALWAYS_FLASH',
4: 'ON_AUTO_FLASH_REDEYE', 5: 'ON_EXTERNAL_FLASH'}
_AE_STATES = {0: 'INACTIVE', 1: 'SEARCHING', 2: 'CONVERGED', 3: 'LOCKED',
4: 'FLASH_REQUIRED', 5: 'PRECAPTURE'}
_BRIGHTNESS_MEAN_ATOL = 5 # Tolerance for brightness mean
_BURST_LEN = 5
_CAPTURE_INTENT_PREVIEW = 1
_CAPTURE_INTENT_STILL_CAPTURE = 2
_FLASH_STATES = {0: 'FLASH_STATE_UNAVAILABLE', 1: 'FLASH_STATE_CHARGING',
2: 'FLASH_STATE_READY', 3: 'FLASH_STATE_FIRED',
4: 'FLASH_STATE_PARTIAL'}
_FORMAT_NAME = 'yuv'
_IMG_SIZE = (640, 360)
_MAX_SINGLE_STRENGTH_PROP_KEY = 'android.flash.singleStrengthMaxLevel'
_MAX_TORCH_STRENGTH_PROP_KEY = 'android.flash.torchStrengthMaxLevel'
_PATCH_H = 0.25 # center 25%
_PATCH_W = 0.25
_PATCH_X = 0.5-_PATCH_W/2
_PATCH_Y = 0.5-_PATCH_H/2
_SINGLE_STRENGTH_CONTROL_THRESHOLD = 1
_STRENGTH_STEPS = 3 # Steps of flash strengths to be tested
_TEST_NAME = os.path.splitext(os.path.basename(__file__))[0]
_TESTING_AE_MODES = (0, 1)
_TORCH_MODE = 2
_TORCH_STRENGTH_CONTROL_THRESHOLD = 1
_TORCH_STRENGTH_MIN = 0
# TODO: b/344675052 - Add torch strength control in do_3a()
def _take_captures(
self, arduino_serial_port, out_surfaces, cam,
img_name_prefix, ae_mode, torch_strength
):
"""Takes video captures and returns the captured images.
Args:
self: ItsBaseTest object; used for lighting control.
arduino_serial_port: serial port pointer; used for lighting control
out_surfaces: list; valid output surfaces for caps.
cam: ItsSession util object.
img_name_prefix: image name to be saved, log_path included.
ae_mode: AE mode to be tested with.
torch_strength: Flash strength that flash should be fired with.
Note that 0 is for baseline capture.
Returns:
caps: list of capture objects as described by cam.do_capture().
"""
# Take image without flash
if torch_strength == 0:
# turn OFF lights to darken scene
lighting_control_utils.set_lighting_state(
arduino_serial_port, self.lighting_ch, 'OFF'
)
cam.do_3a(do_af=False, lock_awb=True, out_surfaces=out_surfaces)
cap_req = capture_request_utils.auto_capture_request()
cap_req[
'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
cap_req['android.control.aeMode'] = 0 # AE_MODE_OFF
cap_req['android.control.awbLock'] = True
logging.debug('Capturing image without flash')
cap = cam.do_capture(cap_req, out_surfaces, reuse_session=True)
# turn the lights back on
lighting_control_utils.set_lighting_state(
arduino_serial_port, self.lighting_ch, 'ON'
)
return [cap]
# Take multiple still captures with torch strength
else:
cam.do_3a(do_af=False, lock_awb=True, flash_mode=_TORCH_MODE,
out_surfaces=out_surfaces)
# turn OFF lights to darken scene
lighting_control_utils.set_lighting_state(
arduino_serial_port, self.lighting_ch, 'OFF'
)
cap_req = capture_request_utils.auto_capture_request()
cap_req['android.control.aeMode'] = ae_mode
cap_req['android.control.captureIntent'] = _CAPTURE_INTENT_PREVIEW
cap_req['android.control.aeLock'] = True
cap_req['android.control.awbLock'] = True # AWB Lock
cap_req['android.flash.mode'] = _TORCH_MODE
cap_req['android.flash.strengthLevel'] = torch_strength
reqs = [cap_req] * _BURST_LEN
logging.debug('Capturing burst with torch strength: %s', torch_strength)
caps = cam.do_capture(reqs, out_surfaces, reuse_session=True)
# turn the lights back on
lighting_control_utils.set_lighting_state(
arduino_serial_port, self.lighting_ch, 'ON'
)
for i, cap in enumerate(caps):
img = image_processing_utils.convert_capture_to_rgb_image(cap)
# Save captured image
image_processing_utils.write_image(img, f'{img_name_prefix}{i}.jpg')
return caps
def _get_img_patch_mean(caps, props):
"""Evaluate captured image by extracting means in the center patch.
Args:
caps: captured list of image object as defined by
ItsSessionUtils.do_capture().
props: Camera properties object.
Returns:
mean: (list of float64) calculated means of Y plane center patch.
"""
flash_means = []
for cap in caps:
metadata = cap['metadata']
exp = int(metadata['android.sensor.exposureTime'])
iso = int(metadata['android.sensor.sensitivity'])
flash_exp_x_iso = []
logging.debug('cap ISO: %d, exp: %d ns', iso, exp)
logging.debug('AE_MODE (cap): %s',
_AE_MODES[metadata['android.control.aeMode']])
ae_state = _AE_STATES[metadata['android.control.aeState']]
logging.debug('AE_STATE (cap): %s', ae_state)
flash_state = _FLASH_STATES[metadata['android.flash.state']]
logging.debug('FLASH_STATE: %s', flash_state)
logging.debug('FLASH_STRENGTH: %s', metadata['android.flash.strengthLevel'])
flash_exp_x_iso = exp*iso
y, _, _ = image_processing_utils.convert_capture_to_planes(
cap, props)
patch = image_processing_utils.get_image_patch(
y, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
flash_mean = image_processing_utils.compute_image_means(
patch)[0]*255
flash_means.append(flash_mean)
logging.debug('Flash exposure X ISO %d', flash_exp_x_iso)
logging.debug('Flash frames Y mean: %.4f', flash_mean)
return flash_means
def _compare_means(formats_means, ae_mode, flash_strengths):
"""Compares the means of the captured images at different strength levels.
If AE_MODE is ON/OFF, capture should show mean differences
in flash strengths. If AE_MODE is ON_AUTO_FLASH, flash
strength should be overwritten hence no mean difference in captures.
Args:
formats_means: list of calculated means of image center patches of all req.
ae_mode: requested AE mode during testing.
flash_strengths: list of flash strength values requested during testing.
Returns:
failure_messages: (list of string) list of error messages.
"""
failure_messages = []
strength_means = [np.average(x) for x in formats_means]
# Intentionally omitting frame-to-frame sameness check of last burst
for i, burst_means in enumerate(formats_means[:-1]):
# Check for strength brightness with averages of same strength captures
if (strength_means[i] >= strength_means[i+1] and
ae_mode in _AE_MODE_FLASH_CONTROL):
msg = (
f'Capture with CONTROL_AE_MODE: {_AE_MODES[ae_mode]}; '
f'Strength {flash_strengths[i]} mean: {strength_means[i]}; '
f'Strength {flash_strengths[i+1]} mean: {strength_means[i+1]}; '
f'Mean of {flash_strengths[i+1]} should be brighter than '
f'Mean of {flash_strengths[i]}!'
)
failure_messages.append(msg)
for j in range(len(burst_means)-1):
# Check for frame-to-frame sameness
diff = abs(burst_means[j] - burst_means[j+1])
if diff > _BRIGHTNESS_MEAN_ATOL:
if ae_mode in _AE_MODE_FLASH_CONTROL:
msg = (
f'Capture with CONTROL_AE_MODE: {_AE_MODES[ae_mode]}; '
f'Strength {flash_strengths[i]} capture {j} mean: '
f'{burst_means[j]},'
f'Strength {flash_strengths[i+1]} capture {j+1} mean: '
f'{burst_means[j+1]}, '
f'Torch strength is not consistent between captures '
f'Diff: {diff}; TOL: {_BRIGHTNESS_MEAN_ATOL}'
)
else:
msg = (
f'Capture with CONTROL_AE_MODE: {_AE_MODES[ae_mode]}. '
f'Strength {flash_strengths[i]} mean: {burst_means[j]}, '
f'Strength {flash_strengths[i+1]} mean: '
f'{burst_means[j+1]}. '
f'Diff: {diff}; TOL: {_BRIGHTNESS_MEAN_ATOL}'
)
failure_messages.append(msg)
return failure_messages
class TorchStrengthTest(its_base_test.ItsBaseTest):
"""Test if torch strength control feature works as intended."""
def test_torch_strength(self):
name_with_path = os.path.join(self.log_path, _TEST_NAME)
with its_session_utils.ItsSession(
device_id=self.dut.serial,
camera_id=self.camera_id,
hidden_physical_id=self.hidden_physical_id) as cam:
props = cam.get_camera_properties()
props = cam.override_with_hidden_physical_camera_props(props)
# check SKIP conditions
max_flash_strength = props[_MAX_SINGLE_STRENGTH_PROP_KEY]
max_torch_strength = props[_MAX_TORCH_STRENGTH_PROP_KEY]
camera_properties_utils.skip_unless(
camera_properties_utils.flash(props) and
max_flash_strength > _SINGLE_STRENGTH_CONTROL_THRESHOLD and
max_torch_strength > _TORCH_STRENGTH_CONTROL_THRESHOLD
)
# establish connection with lighting controller
arduino_serial_port = lighting_control_utils.lighting_control(
self.lighting_cntl, self.lighting_ch
)
failure_messages = []
# testing at 80% of max strength
max_torch_strength = max_torch_strength * 0.8
# list with no torch (baseline), linear strength steps, 0.8 max strength
torch_strengths = [max_torch_strength*i/_STRENGTH_STEPS for i in
range(_STRENGTH_STEPS)]
torch_strengths.append(max_torch_strength)
logging.debug('Testing flash strengths: %s', torch_strengths)
for ae_mode in _TESTING_AE_MODES:
formats_means = []
for strength in torch_strengths:
if (_TORCH_STRENGTH_MIN < strength <=
_TORCH_STRENGTH_CONTROL_THRESHOLD):
logging.debug('Torch strength value <= %d, test case ignored',
_TORCH_STRENGTH_CONTROL_THRESHOLD)
else:
# naming images to be captured
img_name_prefix = (
f'{name_with_path}_ae_mode={ae_mode}_'
f'torch_strength={strength}_'
)
# check if testing image size is supported
output_sizes = capture_request_utils.get_available_output_sizes(
_FORMAT_NAME, props)
if _IMG_SIZE in output_sizes:
width, height = _IMG_SIZE
logging.debug(
'Testing with default image size: %dx%d', width, height
)
else:
width, height = output_sizes[len(output_sizes)//2]
logging.debug(
'Default size not supported, testing with size: %dx%d',
width, height
)
# defining out_surfaces
out_surfaces = {'format': _FORMAT_NAME,
'width': width, 'height': height}
# take capture and evaluate
caps = _take_captures(
self, arduino_serial_port, out_surfaces, cam,
img_name_prefix, ae_mode, strength
)
formats_means.append(_get_img_patch_mean(caps, props))
# Compare means and compose failure messages
failure_messages += _compare_means(formats_means,
ae_mode, torch_strengths)
# turn the lights back on
lighting_control_utils.set_lighting_state(
arduino_serial_port, self.lighting_ch, 'ON')
# assert correct behavior and print error message(s)
if failure_messages:
raise AssertionError('\n'.join(failure_messages))
if __name__ == '__main__':
test_runner.main()