blob: 659de6a7a395ee390981a02c623f302ae1483867 [file] [log] [blame]
# 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 LED snapshot works correctly."""
import logging
import os.path
import camera_properties_utils
import capture_request_utils
import image_processing_utils
import its_base_test
import its_session_utils
import lighting_control_utils
from mobly import test_runner
_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'}
_FLASH_STATES = {0: 'FLASH_STATE_UNAVAILABLE', 1: 'FLASH_STATE_CHARGING',
2: 'FLASH_STATE_READY', 3: 'FLASH_STATE_FIRED',
4: 'FLASH_STATE_PARTIAL'}
_FORMAT_NAMES = ('jpeg', 'yuv')
_IMG_SIZES = ((640, 480), (640, 360))
_VGA_SIZE = (640, 480)
_CH_FULL_SCALE = 255
_TEST_NAME = os.path.splitext(os.path.basename(__file__))[0]
_AE_MODE_ON_AUTO_FLASH = 2
_CAPTURE_INTENT_PREVIEW = 1
_CAPTURE_INTENT_STILL_CAPTURE = 2
_AE_PRECAPTURE_TRIGGER_START = 1
_AE_PRECAPTURE_TRIGGER_IDLE = 0
_FLASH_MEAN_MIN = 50
_FLASH_MEAN_MAX = 200
_WB_MIN = 0.8
_WB_MAX = 1.2
_COLOR_CHANNELS = ('R', 'G', 'B')
def _take_captures(out_surfaces, cam, img_name, flash=False):
"""Takes captures and returns the captured image.
Args:
out_surfaces:
cam: ItsSession util object
img_name: image name to be saved.
flash: True if the capture needs to be taken with Flash ON
Returns:
cap: captured image object as defined by
ItsSessionUtils.do_capture()
"""
cam.do_3a(do_af=False)
if not flash:
cap_req = capture_request_utils.auto_capture_request()
cap_req[
'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
cap = cam.do_capture(cap_req, out_surfaces)
else:
cap = capture_request_utils.take_captures_with_flash(cam, out_surfaces)
img = image_processing_utils.convert_capture_to_rgb_image(cap)
# Save captured image
image_processing_utils.write_image(img, img_name)
return cap
def _is_color_mean_valid(means, color_channel, fmt_name, width, height):
"""Checks if the mean for color_channel is within the range.
Computes means for color_channel specified and checks whether
it is within the acceptable range.
Args:
means: list of means in float
color_channel: String; values must be one of the color
channels in _COLOR_CHANNELS
fmt_name: Format to be tested
width: width of the image to be tested
height: height of the image to be tested
Returns:
True if the color mean is within the range and returns False
if invalid.
"""
if color_channel not in _COLOR_CHANNELS:
raise AssertionError('Invalid color_channel.')
if color_channel == 'R':
color_mean = means[0]
elif color_channel == 'G':
color_mean = means[1]
else:
color_mean = means[2]
if not _FLASH_MEAN_MIN <= color_mean <= _FLASH_MEAN_MAX:
logging.debug('Flash image mean %s not'
' within limits for channel %s.'
' Format: %s,'
' Size: %sx%s', color_mean, color_channel,
fmt_name, width, height)
return False
else:
return True
class LedSnapshotTest(its_base_test.ItsBaseTest):
"""Tests if LED snapshot works correctly.
In this test we capture the failure that the LED snapshot is not too dark,
too bright or producing a strange color tint.
During the test 3 images are captured for each format in _FORMAT_NAMES
and size in _IMG_SIZES:
1. Lights ON, AUTO_FLASH set to OFF -> Baseline capture without any flash.
2. Lights OFF, AUTO_FLASH set to OFF -> Ensures dark lighting conditions
to trigger the flash.
3. Lights OFF, AUTO_FLASH set to ON -> Still capture with flash
For all the 3 pictures we compute the image means and log them.
For the capture with flash triggered, we compare the mean to be within the
minimum and maximum threshold level. The capture with flash should not be too
dark or too bright.
In order to ensure the white balance, the ratio of R/G and B/G is also
compared to be within the pre-decided threshold level.
Failures will be reported if any of the measuremenet is out of range.
"""
def test_led_snapshot(self):
test_name = 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
first_api_level = its_session_utils.get_first_api_level(
self.dut.serial)
camera_properties_utils.skip_unless(
camera_properties_utils.flash(props) and
first_api_level >= its_session_utils.ANDROID14_API_LEVEL)
failure_messages = []
# establish connection with lighting controller
arduino_serial_port = lighting_control_utils.lighting_control(
self.lighting_cntl, self.lighting_ch)
for fmt_name in _FORMAT_NAMES:
for size in _IMG_SIZES:
width, height = size
if not (fmt_name == 'yuv' and size == _VGA_SIZE):
output_sizes = capture_request_utils.get_available_output_sizes(
fmt_name, props, match_ar_size=size)
if not output_sizes:
if size != _VGA_SIZE:
logging.debug('No output sizes for format %s, size %sx%s',
fmt_name, width, height)
continue
else:
raise AssertionError(f'No output sizes for format {fmt_name}, '
f'size {width}x{height}')
# pick smallest size out of available output sizes
width, height = output_sizes[-1]
out_surfaces = {'format': fmt_name, 'width': width, 'height': height}
logging.debug(
'Testing %s format, size: %dx%d', fmt_name, width, height)
# take capture with lights on - no flash
logging.debug(
'Taking reference frame with lights on and no flash.')
img_prefix = f'{test_name}_{fmt_name}_{width}x{height}'
light_on_img_name = f'{img_prefix}_lights_on.jpg'
_take_captures(out_surfaces, cam, light_on_img_name, flash=False)
# turn OFF lights to darken scene
lighting_control_utils.set_lighting_state(
arduino_serial_port, self.lighting_ch, 'OFF')
# take capture with no flash as baseline
logging.debug(
'Taking reference frame with lights off and no auto-flash.')
no_flash_req = capture_request_utils.auto_capture_request()
no_flash_req[
'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE
no_flash_img_name = f'{img_prefix}_no_flash.jpg'
_take_captures(out_surfaces, cam, no_flash_img_name, flash=False)
# take capture with auto flash enabled
logging.debug('Taking capture with auto flash enabled.')
flash_fired = False
flash_img_name = f'{img_prefix}_flash.jpg'
cap = _take_captures(out_surfaces, cam, flash_img_name, flash=True)
img = image_processing_utils.convert_capture_to_rgb_image(cap)
# evaluate captured image
metadata = cap['metadata']
exp = int(metadata['android.sensor.exposureTime'])
iso = int(metadata['android.sensor.sensitivity'])
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)
if flash_state == 'FLASH_STATE_FIRED':
logging.debug('Flash fired')
flash_fired = True
flash_means = image_processing_utils.compute_image_means(img)
logging.debug('Image means with flash: %s', flash_means)
flash_means = [i * _CH_FULL_SCALE for i in flash_means]
logging.debug('Flash capture rgb means: %s', flash_means)
# Verify that R/G and B/G ratios are within the limits
r_g_ratio = flash_means[0]/ flash_means[1]
logging.debug('R/G ratio: %s fmt: %s, WxH: %sx%s',
r_g_ratio, fmt_name, width, height)
b_g_ratio = flash_means[2]/flash_means[1]
logging.debug('B/G ratio: %s fmt: %s, WxH: %sx%s',
b_g_ratio, fmt_name, width, height)
if not _WB_MIN <= r_g_ratio <= _WB_MAX:
failure_messages.append(f'R/G ratio: {r_g_ratio} not within'
f' the limits. Format: {fmt_name},'
f' Size: {width}x{height}')
if not _WB_MIN <= b_g_ratio <= _WB_MAX:
failure_messages.append(f'B/G ratio: {b_g_ratio} not within'
f' the limits. Format: {fmt_name},'
f' Size: {width}x{height}')
# Check whether the image means for each color channel is
# within the limits or not.
valid_color = True
for color in _COLOR_CHANNELS:
valid_color = _is_color_mean_valid(flash_means, color,
fmt_name, width, height)
if not valid_color:
failure_messages.append(
f'Flash image mean not within limits for channel {color}.'
f' Format: {fmt_name},Size: {width}x{height}')
if not flash_fired:
raise AssertionError(
'Flash was not fired. Format:{fmt_name}, Size:{width}x{height}')
# turn the lights back on
lighting_control_utils.set_lighting_state(
arduino_serial_port, self.lighting_ch, 'ON')
# assert correct behavior for all formats
if failure_messages:
raise AssertionError('\n'.join(failure_messages))
if __name__ == '__main__':
test_runner.main()