blob: 16c5fa4a65578e809c006cabd86e1d59190de904 [file]
# 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.
"""Utility functions for multi camera switch tests."""
import logging
import math
import numpy
import camera_properties_utils
import image_processing_utils
import image_fov_utils
import its_session_utils
import opencv_processing_utils
_CH_FULL_SCALE = 255
_CONTROL_AF_STATE_PASSIVE_SCAN = 1
_CONTROL_AF_STATE_PASSIVE_FOCUSED = 2
_CONVERGED_STATE = 2
_AF_CONVERGED_STATE = [_CONTROL_AF_STATE_PASSIVE_SCAN,
_CONTROL_AF_STATE_PASSIVE_FOCUSED]
def check_orientation_and_flip(props, img, img_name_stem, suffix):
"""Checks the sensor orientation and flips image.
The preview stream captures are flipped based on the sensor
orientation while using the front camera. In such cases, check the
sensor orientation and flip the image if needed.
Args:
props: obj; camera properties object.
img: numpy array; image.
img_name_stem: str; prefix for the img name to be saved.
suffix: str; suffix for naming image.
Returns:
numpy array of the image.
"""
img = image_processing_utils.mirror_preview_image_by_sensor_orientation(
props['android.sensor.orientation'], img)
image_processing_utils.write_image(img / _CH_FULL_SCALE,
f'{img_name_stem}_{suffix}.png')
return img
def do_ae_check(img1, img2, file_stem, patch_color, props, suffix1,
suffix2, rel_tol, abs_tol):
"""Check two images' luma change is within specified tolerance.
Args:
img1: first image.
img2: second image.
file_stem: str; path to file.
patch_color: str; color of the patch to be tested.
props: obj; camera properties object.
suffix1: str; suffix for the first image file name.
suffix2: str; suffix for the second image file name.
rel_tol: float; relative threshold for delta between brightness.
abs_tol: float; absolute threshold for delta between brightness.
Returns:
failed_ae_msg: str; failed AE check messages if any. None otherwise.
y1_avg: float; y_avg value for the first image.
y2_avg: float; y_avg value for the second image.
"""
failed_ae_msg = []
y1 = opencv_processing_utils.extract_y(
img1, f'{file_stem}_{suffix1}_y.png')
y1_avg = numpy.average(y1)
logging.debug('%s y avg: %.4f', suffix1, y1_avg)
y2 = opencv_processing_utils.extract_y(
img2, f'{file_stem}_{suffix2}_y.png')
y2_avg = numpy.average(y2)
logging.debug('%s y avg: %.4f', suffix2, y2_avg)
y_avg_change_percent = (abs(y2_avg - y1_avg) / y1_avg) * 100
logging.debug('Y avg change percentage: %.4f', y_avg_change_percent)
if not math.isclose(y1_avg, y2_avg, rel_tol=rel_tol, abs_tol=abs_tol):
failed_msg = ('Y avg change is greater than threshold value for '
f'patches: {patch_color} '
f'diff: {abs(y2_avg - y1_avg):.4f} '
f'ATOL: {abs_tol} '
f'RTOL: {rel_tol} '
f'{suffix1} y avg: {y1_avg:.4f} '
f'{suffix2} y avg: {y2_avg:.4f} ')
# If the device supports both ae_regions and awb_regions,
# then fail for all 4 patches.
if camera_properties_utils.awb_regions(props):
failed_ae_msg.append(failed_msg)
else:
# If only ae_regions is supported, then fail only for gray patch
if patch_color == 'gray':
failed_ae_msg.append(failed_msg)
return failed_ae_msg, y1_avg, y2_avg
def do_af_check(img1, img2, suffix1, suffix2):
"""Checks the AF behavior between two images.
Args:
img1: image captured from first camera.
img2: image captured from second camera.
suffix1: str; suffix used to save the first image.
suffix2: str; suffix used to save the second image.
Returns:
failed_af_msg: Failed AF check messages if any. None otherwise.
sharpness1: sharpness value for first image.
sharpness2: sharpness value for second image.
"""
failed_af_msg = []
sharpness1 = image_processing_utils.compute_image_sharpness(img1)
logging.debug('Sharpness for %s image: %.2f', suffix1, sharpness1)
sharpness2 = image_processing_utils.compute_image_sharpness(img2)
logging.debug('Sharpness for %s image: %.2f', suffix2, sharpness2)
if sharpness2 < sharpness1:
failed_af_msg.append(f'Sharpness should be higher for {suffix2} lens. '
f'{suffix2} sharpness: {sharpness2:.4f} '
f'{suffix1} sharpness: {sharpness1:.4f}')
return failed_af_msg, sharpness1, sharpness2
def do_awb_check(img1, img2, c_atol, patch_color, suffix1, suffix2):
"""Checks total chroma (saturation) difference between two images.
Args:
img1: first image.
img2: second image.
c_atol: float; threshold for delta C.
patch_color: str; color of the patch to be tested.
suffix1: str; suffix for the first image.
suffix2: str; suffix for the second image.
Returns:
failed_awb_msg: failed AWB check messages or None.
"""
failed_awb_msg = []
l1, a1, b1 = image_processing_utils.get_lab_means(img1, suffix1)
l2, a2, b2 = image_processing_utils.get_lab_means(img2, suffix2)
# Calculate Delta C
delta_c = numpy.sqrt(abs(a1 - a2)**2 + abs(b1 - b2)**2)
logging.debug('Delta C: %.4f', delta_c)
if delta_c > c_atol:
failed_awb_msg.append('Delta C is greater than the threshold value for '
f'patch: {patch_color} '
f'Delta C ATOL: {c_atol} '
f'Delta C: {delta_c:.4f} '
f'{suffix1} L, a, b means: {l1:.4f}, '
f'{a1:.4f}, {b1:.4f}'
f'{suffix2} L, a, b means: {l2:.4f}, '
f'{a2:.4f}, {b2:.4f}')
return failed_awb_msg
def extract_main_patch(corners, ids, img_rgb, img_path, suffix):
"""Extracts the main rectangle patch from the captured frame.
Find aruco markers in the captured image and detects if the
expected number of aruco markers have been found or not.
It then, extracts the main rectangle patch and saves it
without the aruco markers in it.
Args:
corners: list of detected corners.
ids: list of int ids for each ArUco markers in the input_img.
img_rgb: An openCV image in RGB order.
img_path: Path to save the image.
suffix: str; suffix used to save the image.
Returns:
rectangle_patch: numpy float image array of the rectangle patch.
"""
rectangle_patch = opencv_processing_utils.get_patch_from_aruco_markers(
img_rgb, corners, ids)
patch_path = img_path.with_name(
f'{img_path.stem}_{suffix}_patch{img_path.suffix}')
image_processing_utils.write_image(rectangle_patch/_CH_FULL_SCALE, patch_path)
return rectangle_patch
def find_aruco_markers(img, img_path, suffix):
"""Detect ArUco markers in the input image.
Args:
img: input img with ArUco markers.
img_path: path to save the image.
suffix: suffix used to save the image.
Returns:
corners: list of detected corners.
ids: list of int ids for each ArUco markers in the input_img.
"""
aruco_path = img_path.with_name(
f'{img_path.stem}_{suffix}_aruco{img_path.suffix}')
corners, ids, _ = opencv_processing_utils.find_aruco_markers(
img, aruco_path)
return corners, ids
def get_error_msg(failed_awb_msg, failed_ae_msg):
""""Returns the error message string.
Args:
failed_awb_msg: list of awb error msgs.
failed_ae_msg: list of ae error msgs.
Returns:
error_msg: str; error_msg string.
"""
error_msg = ''
if failed_awb_msg:
error_msg = f'{error_msg}----AWB Check----\n'
for msg in failed_awb_msg:
error_msg = f'{error_msg}{msg}\n'
if failed_ae_msg:
error_msg = f'{error_msg}----AE Check----\n'
for msg in failed_ae_msg:
error_msg = f'{error_msg}{msg}\n'
return error_msg
def check_lens_switch_conditions(props, first_api_level, zoom_range_lenses,
logical_camera_found):
"""Check the camera properties for lens switch conditions.
Camera only switches if 3A converges
Args:
props: Camera properties dictionary.
first_api_level: First API level.
zoom_range_lenses: Tuple of two zoom ratio.
logical_camera_found: Boolean indicating if logical camera is found.
Raises:
SkipTest: If the device doesn't support the required properties or API
level.
"""
camera_properties_utils.skip_unless(
first_api_level >= its_session_utils.ANDROID16_API_LEVEL and
camera_properties_utils.zoom_ratio_range(props) and
camera_properties_utils.logical_multi_camera(props) and
camera_properties_utils.ae_regions(props) and
logical_camera_found)
# Check the zoom range
zoom_range = props['android.control.zoomRatioRange']
logging.debug('zoomRatioRange: %s', zoom_range)
camera_properties_utils.skip_unless(
len(zoom_range) > 1 and
(zoom_range[0] <= zoom_range_lenses[0] <= zoom_range[1]) and
(zoom_range[0] <= zoom_range_lenses[1] <= zoom_range[1]))
def find_crossover_point(cam, capture_results):
"""Find the crossover point where the physical camera changes.
Analyze each frame extracted from the recording to detect the point
at which the camera's active physical ID changes alongside an
increasing zoom ratio. A successful crossover is identified only
when the 3A algorithms have converged at the frame where the camera
switch occurs.
Args:
cam: An open device session.
capture_results: List of capture results.
Returns:
A tuple of (lens_changed, counter)
lens_changed: Boolean indicating if the lens changed.
counter: number of frame where the crossover occurred.
Raises: AssertionError if the 3A did not converge at crossover point.
"""
physical_id_before = None
counter = 0
lens_changed = False
for capture_result in capture_results:
counter += 1
ae_state = capture_result['android.control.aeState']
awb_state = capture_result['android.control.awbState']
af_state = capture_result['android.control.afState']
physical_id = capture_result[
'android.logicalMultiCamera.activePhysicalId']
zoom_ratio = float(capture_result['android.control.zoomRatio'])
logging.debug('Active physical id %s frame %s AE, AWB, AF: %s, %s, %s',
physical_id, counter, ae_state, awb_state, af_state)
if not physical_id_before:
physical_id_before = physical_id
if physical_id_before == physical_id:
continue
physical_props_id_before = cam.get_camera_properties_by_id(
physical_id_before
)
physical_props_id = cam.get_camera_properties_by_id(physical_id)
logging.debug(
'Active physical id %s changed to %s at frame %s and zoom ratio %f',
physical_id_before, physical_id, counter, zoom_ratio
)
# Avoid getting HAL-simulated camera by checking field of view change
camera_fov_before = image_fov_utils.calc_camera_fov_from_metadata(
capture_results[counter - 2], physical_props_id_before)
camera_fov = image_fov_utils.calc_camera_fov_from_metadata(
capture_result, physical_props_id)
physical_id_before = physical_id
if camera_fov_before != camera_fov:
logging.debug('Cameras with different field of view (%s != %s) crossed.',
camera_fov_before, camera_fov)
print(f'test_multi_camera_switch_crossover_zoom_ratio: {zoom_ratio:.2f}')
if (ae_state == awb_state == _CONVERGED_STATE) and (
af_state in _AF_CONVERGED_STATE):
lens_changed = True
logging.debug('3A converged at crossover. AE, AWB, AF state: '
'%s, %s, %s', ae_state, awb_state, af_state)
break
else:
logging.debug('AE, AWB, AF state: %s, %s, %s',
ae_state, awb_state, af_state)
raise AssertionError('3A did not converge at crossover.')
return lens_changed, counter
def get_camera_properties_and_log(cam, capture_results, file_list, counter,
lens_suffix1, lens_suffix2):
"""Get camera properties for the specific cameras and log the information.
Args:
cam: An open device session.
capture_results: List of capture results.
file_list: List of captured image files.
counter: Counter for the crossover point.
lens_suffix1: Suffix for the first camera.
lens_suffix2: Suffix for the second camera.
Returns:
Tuple of camera properties for both cameras.
"""
# Get data for the second to last frame before the switch happened
img1_file = file_list[counter-2]
capture_result_img1 = capture_results[counter-2]
img1_phy_id = (
capture_result_img1['android.logicalMultiCamera.activePhysicalId']
)
physical_props_img1 = cam.get_camera_properties_by_id(img1_phy_id)
min_focus_distance_img1 = (
physical_props_img1['android.lens.info.minimumFocusDistance']
)
logging.debug('Min focus distance for %s phy_id: %s is %f',
lens_suffix1, img1_phy_id, min_focus_distance_img1)
logging.debug('Capture results %s crossover: %s',
lens_suffix1, capture_result_img1)
logging.debug('Capture results %s crossover: %s',
lens_suffix2, capture_results[counter-1])
# Get data for last frame where the switch happened
img2_file = file_list[counter-1]
capture_result_img2 = capture_results[counter-1]
logging.debug('Capture results %s crossover converged: %s',
lens_suffix2, capture_result_img2)
img2_phy_id = (
capture_result_img2['android.logicalMultiCamera.activePhysicalId'])
physical_props_img2 = cam.get_camera_properties_by_id(img2_phy_id)
min_focus_distance_img2 = (
physical_props_img2['android.lens.info.minimumFocusDistance']
)
logging.debug('Min focus distance for %s phy_id: %s is %f',
lens_suffix2, img2_phy_id, min_focus_distance_img2)
return img1_file, img2_file, min_focus_distance_img2