blob: 89c318049c6e5b17279e13a314ce88955f92f771 [file]
# Copyright 2023 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 that the switch from UW to W has similar RGB values."""
import logging
import os.path
import pathlib
from mobly import test_runner
import numpy as np
import ip_chart_extraction_utils as ce
import ip_metrics_utils
import its_base_test
import camera_properties_utils
import image_processing_utils
import its_session_utils
import gen2_rig_controller_utils
import multi_camera_switch_utils
import preview_processing_utils
_AWB_DIFF_THRESHOLD = 4
_BRIGHTNESS_DIFF_THRESHOLD = 4
_DYNAMIC_PATCH_MID_TONE_START_IDX = 5
_DYNAMIC_PATCH_MID_TONE_END_IDX = 15
_LENS_SUFFIX_UW = 'uw'
_LENS_SUFFIX_W = 'w'
_MP4_FORMAT = '.mp4'
_NAME = os.path.splitext(os.path.basename(__file__))[0]
_ZOOM_RANGE_UW_W = (0.95, 2.05) # UW/W crossover range
_ZOOM_STEP = 0.01
def _get_mean_brightness(patch_list):
"""Calculate mean brightness from patch list.
Args:
patch_list: list; of patches from grey scale.
Returns:
brightness value: float; mean brightness value.
"""
brightness_values = []
for patch in patch_list:
mean_l, _, _ = ip_metrics_utils.get_lab_mean_values(patch)
brightness_values.append(mean_l)
return np.mean(
brightness_values[
_DYNAMIC_PATCH_MID_TONE_START_IDX:_DYNAMIC_PATCH_MID_TONE_END_IDX
])
def _do_brightness_check(patch_list_w, patch_list_uw):
"""Computes brightness diff between two sets of capture images.
Args:
patch_list_w: list; camera dynamic range patch cells.
patch_list_uw: list; second set of camera dynamic range patch cells.
Returns:
marginal_brightness_pass: bool; True if passing marginally.
marginal_pass_msg: string; String indicating marginal pass.
e_msg: string; Error message if test fails.
"""
brightness_1st = _get_mean_brightness(patch_list_w)
brightness_2nd = _get_mean_brightness(patch_list_uw)
# Below print statements are for logging purpose.
# Do not replace with logging.
print(f'{_NAME}_ae_w_y_avgs: ', brightness_1st)
print(f'{_NAME}_ae_uw_y_avgs: ', brightness_2nd)
# Check that the brightness difference is smaller than the max threshold
brightness_diff = brightness_1st - brightness_2nd
logging.debug('Brightness difference between w and uw: %.2f',
brightness_diff)
e_msg = ''
marginal_pass_msg = ''
marginal_brightness_pass = False
if (abs(brightness_diff) > (
_BRIGHTNESS_DIFF_THRESHOLD * its_session_utils.MARGINAL_PASS_FACTOR)
and abs(brightness_diff) <= (_BRIGHTNESS_DIFF_THRESHOLD)):
marginal_brightness_pass = True
marginal_pass_msg += (
f'Marginally passing brightness check. '
f'Actual: {brightness_diff:.2f}, '
f'Expected: {_BRIGHTNESS_DIFF_THRESHOLD:.1f}. ')
if brightness_diff > _BRIGHTNESS_DIFF_THRESHOLD:
e_msg += (
f'The brightness difference between wide and ultra-wide cameras'
f'for greyscale cells exceeds the threshold. '
f'Actual: {brightness_diff:.2f}, '
f'Expected: {_BRIGHTNESS_DIFF_THRESHOLD:.1f}'
)
return marginal_brightness_pass, marginal_pass_msg, e_msg
def _do_awb_check(mean_white_balance_diff, marginal_pass_msg, e_msg):
"""Computes AWB diff between two sets of capture images.
Args:
mean_white_balance_diff: float; AWB diff between two images.
marginal_pass_msg: string; String indicating marginal pass.
e_msg: string; Error message if test fails.
Returns:
marginal_awb_pass: bool; True if passing marginally.
marginal_pass_msg: string; String indicating marginal pass.
e_msg: string; Error message if test fails.
"""
logging.debug('Mean white balance diff: %f', mean_white_balance_diff)
print(f'{_NAME}_mean_white_balance_diff: {mean_white_balance_diff}')
marginal_awb_pass = False
if (abs(mean_white_balance_diff) > (
_AWB_DIFF_THRESHOLD * its_session_utils.MARGINAL_PASS_FACTOR)
and abs(mean_white_balance_diff) <= (_AWB_DIFF_THRESHOLD)):
marginal_awb_pass = True
marginal_pass_msg += (
f'Marginally passing awb check. '
f'Actual: {mean_white_balance_diff:.2f}, '
f'Expected: {_AWB_DIFF_THRESHOLD:.1f}. '
)
if abs(mean_white_balance_diff) > _AWB_DIFF_THRESHOLD:
e_msg += (
f'The AWB difference between wide and ultra-wide cameras'
f'for greyscale cells exceeds the threshold. '
f'Actual: {mean_white_balance_diff:.2f}, '
f'Expected: {_AWB_DIFF_THRESHOLD:.1f}'
)
return marginal_awb_pass, marginal_pass_msg, e_msg
class MultiCameraSwitchTest(its_base_test.ItsBaseTest):
"""Test that the switch between cameras has similar RGB values.
This test uses various zoom ratios within range android.control.zoomRatioRange
to capture images and find the point when the physical camera changes
to determine the crossover point of change from UW to W.
It does preview recording at UW and W crossover point to verify that
the AE, AWB and AF behavior remains the same.
"""
def test_multi_camera_switch(self):
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
first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
camera_facing = props['android.lens.facing']
logging.debug('Camera facing: %s', camera_facing)
ultrawide_camera_found = cam.has_ultrawide_camera(
facing=camera_facing
)
logging.debug('Ultrawide camera found: %s', ultrawide_camera_found)
multi_camera_switch_utils.check_lens_switch_conditions(
props, first_api_level, _ZOOM_RANGE_UW_W, ultrawide_camera_found)
# Initialize rotation rig
gen2_rig_controller_utils.setup_gen2_rig_with_cam(self, cam)
# Set up scene and configure preview size
preview_test_size = preview_processing_utils.get_max_preview_test_size(
cam, self.camera_id)
cam.do_3a()
try:
# Start dynamic preview recording and collect results
capture_results, file_list = (
preview_processing_utils.preview_over_zoom_range(
self.dut, cam, preview_test_size, _ZOOM_RANGE_UW_W[0],
_ZOOM_RANGE_UW_W[1], _ZOOM_STEP, self.log_path)
)
# Find the crossover point where the camera switches
lens_changed, counter = (
multi_camera_switch_utils.find_crossover_point(
cam, capture_results))
except (AssertionError, RuntimeError) as e:
# Remove all the files except mp4 recording in case of any error
for filename in os.listdir(self.log_path):
file_path = os.path.join(self.log_path, filename)
if os.path.isfile(file_path) and not filename.endswith(_MP4_FORMAT):
os.remove(file_path)
raise AssertionError('Error during crossover check') from e
# Raise error if lens did not switch within the range
# _ZOOM_RANGE_UW_W
# TODO(ruchamk): Add lens_changed to the CameraITS metrics
if not lens_changed:
e_msg = 'Crossover point not found. Try running the test again!'
raise AssertionError(e_msg)
# Process capture results and get camera properties
img_uw_file, img_w_file, _ = (
multi_camera_switch_utils.get_camera_properties_and_log(
cam, capture_results, file_list, counter,
_LENS_SUFFIX_UW, _LENS_SUFFIX_W)
)
# Remove unwanted frames and only save the UW and
# W crossover point frames along with mp4 recording
its_session_utils.remove_frame_files(self.log_path, [
os.path.join(self.log_path, img_uw_file),
os.path.join(self.log_path, img_w_file)])
# Add suffix to the UW and W image files
uw_path = pathlib.Path(os.path.join(self.log_path, img_uw_file))
uw_name = uw_path.with_name(f'{uw_path.stem}_uw{uw_path.suffix}')
os.rename(os.path.join(self.log_path, img_uw_file), uw_name)
w_path = pathlib.Path(os.path.join(self.log_path, img_w_file))
w_name = w_path.with_name(f'{w_path.stem}_w{w_path.suffix}')
os.rename(os.path.join(self.log_path, img_w_file), w_name)
# Convert UW and W img to numpy array
uw_img = image_processing_utils.convert_image_to_numpy_array(
str(uw_name))
w_img = image_processing_utils.convert_image_to_numpy_array(
str(w_name))
# Check the sensor orientation and flip image
# TODO(leslieshaw): Check to see if flip is necessary
if (props['android.lens.facing'] ==
camera_properties_utils.LENS_FACING['FRONT']):
img_name_stem = os.path.join(self.log_path, 'flipped_preview_uw')
uw_img = image_processing_utils.check_orientation_and_flip(
props, uw_img, img_name_stem
)
img_name_stem = os.path.join(self.log_path, 'flipped_preview_w')
w_img = image_processing_utils.check_orientation_and_flip(
props, w_img, img_name_stem
)
w_dynamic_range_patch_cells = (
ce.get_cropped_dynamic_range_patch_cells(
w_name, self.log_path, _LENS_SUFFIX_W)
)
uw_dynamic_range_patch_cells = (
ce.get_cropped_dynamic_range_patch_cells(
uw_name, self.log_path, _LENS_SUFFIX_UW)
)
# Get brightness diff between w and uw captures
marginal_brightness_pass, marginal_pass_msg, e_msg = _do_brightness_check(
w_dynamic_range_patch_cells, uw_dynamic_range_patch_cells)
# Get white balance diff between default and jca captures
mean_white_balance_diff = ip_metrics_utils.do_white_balance_check(
w_dynamic_range_patch_cells, uw_dynamic_range_patch_cells,
_LENS_SUFFIX_W, _LENS_SUFFIX_UW
)
marginal_awb_pass, marginal_pass_msg, e_msg = _do_awb_check(
mean_white_balance_diff, marginal_pass_msg, e_msg
)
# Raise error message if any
if e_msg:
raise AssertionError(e_msg)
elif (marginal_brightness_pass or marginal_awb_pass):
logging.warning('%s\n %s', its_session_utils.MARGINAL_PASSING_MESSAGE,
marginal_pass_msg)
if __name__ == '__main__':
test_runner.main()