blob: eaa553110d30dc8162fc81c9a7b73b1ce607a8b2 [file] [log] [blame]
# Copyright 2019 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.
"""Verifies per_frame_control."""
import logging
import os.path
import matplotlib
from matplotlib import pylab
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
_AE_STATE_CONVERGED = 2
_AE_STATE_FLASH_REQUIRED = 4
_DELTA_GAIN_THRESH = 3 # >3% gain change --> luma change in same dir.
_DELTA_LUMA_THRESH = 3 # 3% frame-to-frame noise test_burst_sameness_manual.
_DELTA_NO_GAIN_THRESH = 1 # <1% gain change --> min luma change.
_NAME = os.path.splitext(os.path.basename(__file__))[0]
_NS_TO_MS = 1.0E-6
_NUM_CAPS = 1
_NUM_FRAMES = 30
_PATCH_H = 0.1 # Center 10%.
_PATCH_W = 0.1
_PATCH_X = 0.5 - _PATCH_W/2
_PATCH_Y = 0.5 - _PATCH_H/2
_RAW_NIBBLE_SIZE = 6 # Used to increase NUM_CAPS & decrease NUM_FRAMES for RAW.
_RAW_GR_CH = 1
_VALID_LUMA_MIN = 0.1
_VALID_LUMA_MAX = 0.9
_YUV_Y_CH = 0
def _check_delta_luma_vs_delta_gain(fmt, j, lumas, total_gains):
"""Determine if luma and gain move together for current frame."""
delta_gain = total_gains[j] - total_gains[j-1]
delta_luma = lumas[j] - lumas[j-1]
delta_gain_rel = delta_gain / total_gains[j-1] * 100 # %
delta_luma_rel = delta_luma / lumas[j-1] * 100 # %
# luma and total_gain should change in same direction
if abs(delta_gain_rel) > _DELTA_GAIN_THRESH:
logging.debug('frame %d: %.2f%% delta gain, %.2f%% delta luma',
j, delta_gain_rel, delta_luma_rel)
if delta_gain * delta_luma < 0.0:
return (f"{fmt['format']}: frame {j}: gain {total_gains[j-1]:.1f} "
f'-> {total_gains[j]:.1f} ({delta_gain_rel:.1f}%), '
f'luma {lumas[j-1]} -> {lumas[j]} ({delta_luma_rel:.2f}%) '
f'GAIN/LUMA OPPOSITE DIR')
elif abs(delta_gain_rel) < _DELTA_NO_GAIN_THRESH:
logging.debug('frame %d: <|%.1f%%| delta gain, %.2f%% delta luma', j,
_DELTA_NO_GAIN_THRESH, delta_luma_rel)
if abs(delta_luma_rel) > _DELTA_LUMA_THRESH:
return (f"{fmt['format']}: frame {j}: gain {total_gains[j-1]:.1f} "
f'-> {total_gains[j]:.1f} ({delta_gain_rel:.1f}%), '
f'luma {lumas[j-1]} -> {lumas[j]} ({delta_luma_rel:.2f}%), '
f'<|{_DELTA_NO_GAIN_THRESH:.1f}%| GAIN, '
f'>|{_DELTA_LUMA_THRESH:.1f}%| LUMA DELTA')
else:
logging.debug('frame %d: %.1f%% delta gain, %.2f%% delta luma',
j, delta_gain_rel, delta_luma_rel)
return None
def _determine_test_formats(cam, props, raw_avlb, debug):
"""Determines the capture formats to test.
Args:
cam: Camera capture object.
props: Camera properties dict.
raw_avlb: Boolean for if RAW captures are available.
debug: Boolean for whether in debug mode.
Returns:
fmts: List of formats.
"""
largest_yuv = capture_request_utils.get_largest_yuv_format(props)
match_ar = (largest_yuv['width'], largest_yuv['height'])
fmt = capture_request_utils.get_smallest_yuv_format(
props, match_ar=match_ar)
if raw_avlb and debug:
return (cam.CAP_RAW, fmt)
else:
return (fmt,)
def _tabulate_frame_data(metadata, luma, raw_cap, debug):
"""Puts relevant frame data into a dictionary."""
ae_state = metadata['android.control.aeState']
iso = metadata['android.sensor.sensitivity']
isp_gain = metadata['android.control.postRawSensitivityBoost'] / 100
exp_time = metadata['android.sensor.exposureTime'] * _NS_TO_MS
total_gain = iso * exp_time
if not raw_cap:
total_gain *= isp_gain
awb_state = metadata['android.control.awbState']
frame = {
'awb_gains': metadata['android.colorCorrection.gains'],
'ccm': metadata['android.colorCorrection.transform'],
'fd': metadata['android.lens.focusDistance'],
}
# Convert CCM from rational to float, as numpy arrays.
awb_ccm = np.array(capture_request_utils.rational_to_float(
frame['ccm'])).reshape(3, 3)
logging.debug('AE: %d ISO: %d ISP_sen: %d exp: %4fms tot_gain: %f luma: %f',
ae_state, iso, isp_gain, exp_time, total_gain, luma)
logging.debug('fd: %f', frame['fd'])
logging.debug('AWB state: %d, AWB gains: %s\n AWB matrix: %s', awb_state,
str(frame['awb_gains']), str(awb_ccm))
if debug:
logging.debug('Tonemap curve: %s', str(metadata['android.tonemap.curve']))
return frame, ae_state, total_gain
def _compute_frame_luma(cap, props, raw_cap):
"""Determines the luma for the center patch of the frame.
RAW captures use GR plane, YUV captures use Y plane.
Args:
cap: Camera capture object.
props: Camera properties dict.
raw_cap: Boolean for capture is RAW or YUV.
Returns:
luma: Luma value for center patch of image.
"""
if raw_cap:
plane = image_processing_utils.convert_capture_to_planes(
cap, props=props)[_RAW_GR_CH]
else:
plane = image_processing_utils.convert_capture_to_planes(cap)[_YUV_Y_CH]
patch = image_processing_utils.get_image_patch(
plane, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
return image_processing_utils.compute_image_means(patch)[0]
def _plot_data(lumas, gains, fmt, log_path):
"""Plots lumas and gains data for this test.
Args:
lumas: List of luma data from captures.
gains: List of gain data from captures.
fmt: String to identify 'YUV' or 'RAW' plots.
log_path: Location to store data.
"""
norm_gains = [x / max(gains) * max(lumas) for x in gains]
pylab.figure(fmt)
pylab.plot(range(len(lumas)), lumas, '-g.', label='Center patch brightness')
pylab.plot(range(len(gains)), norm_gains, '-r.',
label='Metadata AE setting product')
pylab.title(_NAME + ' ' + fmt)
pylab.xlabel('frame index')
# expand y axis for low delta results
ymin = min(norm_gains + lumas)
ymax = max(norm_gains + lumas)
yavg = (ymax + ymin) / 2.0
if ymax - ymin < 3 * _DELTA_LUMA_THRESH/100:
ymin = round(yavg - 1.5 * _DELTA_LUMA_THRESH/100, 3)
ymax = round(yavg + 1.5 * _DELTA_LUMA_THRESH/100, 3)
pylab.ylim(ymin, ymax)
pylab.legend()
matplotlib.pyplot.savefig(
'%s_plot_%s.png' % (os.path.join(log_path, _NAME), fmt))
def _is_awb_af_stable(cap_info, i):
"""Determines if Auto White Balance and Auto Focus are stable."""
awb_gains_i_1 = cap_info[i-1]['awb_gains']
awb_gains_i = cap_info[i]['awb_gains']
return (np.allclose(awb_gains_i_1, awb_gains_i, rtol=0.01) and
cap_info[i-1]['ccm'] == cap_info[i]['ccm'] and
np.isclose(cap_info[i-1]['fd'], cap_info[i]['fd'], rtol=0.01))
class AutoPerFrameControlTest(its_base_test.ItsBaseTest):
"""Tests PER_FRAME_CONTROL properties for auto capture requests.
Takes a sequence of images with auto capture request.
Determines if luma and gain settings move in same direction for large setting
changes.
Small settings changes should result in small changes in luma.
Threshold for checking is DELTA_GAIN_THRESH. Theshold where not change is
expected is DELTA_NO_GAIN_THRESH.
While not included in this test, if camera debug is required:
MANUAL_POSTPROCESSING capability is implied since
camera_properties_utils.read_3a is valid for test.
debug can also be performed with a defined tonemap curve:
req['android.tonemap.mode'] = 0
gamma = sum([[i/63.0,math.pow(i/63.0,1/2.2)] for i in xrange(64)],[])
req['android.tonemap.curve'] = {'red': gamma, 'green': gamma,
'blue': gamma}
"""
def test_auto_per_frame_control(self):
logging.debug('Starting %s', _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)
log_path = self.log_path
# Check SKIP conditions.
camera_properties_utils.skip_unless(
camera_properties_utils.per_frame_control(props) and
camera_properties_utils.read_3a(props))
# Load chart for scene.
its_session_utils.load_scene(
cam, props, self.scene, self.tablet, self.chart_distance)
debug = self.debug_mode
raw_avlb = camera_properties_utils.raw16(props)
fmts = _determine_test_formats(cam, props, raw_avlb, debug)
failed = []
for i, fmt in enumerate(fmts):
logging.debug('fmt: %s', str(fmt['format']))
cam.do_3a()
req = capture_request_utils.auto_capture_request()
cap_info = {}
ae_states = []
lumas = []
total_gains = []
num_caps = _NUM_CAPS
num_frames = _NUM_FRAMES
raw_cap = i == 0 and raw_avlb and debug
# Break up caps if RAW to reduce bandwidth requirements.
if raw_cap:
num_caps = _NUM_CAPS * _RAW_NIBBLE_SIZE
num_frames = _NUM_FRAMES // _RAW_NIBBLE_SIZE
# Capture frames and tabulate info.
for j in range(num_caps):
caps = cam.do_capture([req] * num_frames, fmt)
for k, cap in enumerate(caps):
idx = k + j * num_frames
logging.debug('=========== frame %d ==========', idx)
luma = _compute_frame_luma(cap, props, raw_cap)
frame, ae_state, total_gain = _tabulate_frame_data(
cap['metadata'], luma, raw_cap, debug)
cap_info[idx] = frame
ae_states.append(ae_state)
lumas.append(luma)
total_gains.append(total_gain)
# Save image.
img = image_processing_utils.convert_capture_to_rgb_image(
cap, props=props)
image_processing_utils.write_image(img, '%s_frame_%s_%d.jpg' % (
os.path.join(log_path, _NAME), fmt['format'], idx))
_plot_data(lumas, total_gains, fmt['format'], log_path)
# Check correct behavior
logging.debug('fmt: %s', str(fmt['format']))
for j in range(1, num_caps * num_frames):
if _is_awb_af_stable(cap_info, j):
error_msg = _check_delta_luma_vs_delta_gain(
fmt, j, lumas, total_gains)
if error_msg:
failed.append(error_msg)
else:
logging.debug('frame %d -> %d: AWB/AF changed', j-1, j)
for j, luma in enumerate(lumas):
if ((ae_states[j] == _AE_STATE_CONVERGED or
ae_states[j] == _AE_STATE_FLASH_REQUIRED) and
(_VALID_LUMA_MIN > luma or luma > _VALID_LUMA_MAX)):
failed.append(
f"{fmt['format']}: frame {j} AE converged luma {luma}. "
f'Valid range: ({_VALID_LUMA_MIN}, {_VALID_LUMA_MAX})'
)
if failed:
logging.error('Error summary')
for fail in failed:
logging.error('%s', fail)
raise AssertionError
if __name__ == '__main__':
test_runner.main()