blob: 4af6543b5e5fe19f1f2884e2a2a4265bc40cd08c [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.
import os.path
import its.caps
import its.device
import its.image
import its.objects
import matplotlib
from matplotlib import pylab
import numpy as np
AE_STATE_CONVERGED = 2
AE_STATE_FLASH_REQUIRED = 4
DELTA_GAIN_THRESH = 0.03 # >3% gain change --> luma change in same dir
DELTA_LUMA_THRESH = 0.03 # 3% frame-to-frame noise test_burst_sameness_manual
DELTA_NO_GAIN_THRESH = 0.01 # <1% gain change --> min luma change
LSC_TOL = 0.005 # allow <0.5% change in lens shading correction
NAME = os.path.basename(__file__).split('.')[0]
NUM_CAPS = 1
NUM_FRAMES = 30
VALID_STABLE_LUMA_MIN = 0.1
VALID_STABLE_LUMA_MAX = 0.9
def lsc_unchanged(lsc_avlb, lsc, idx):
"""Determine if lens shading correction unchanged.
Args:
lsc_avlb: bool; True if lens shading correction available
lsc: list; lens shading correction matrix
idx: int; frame index
Returns:
boolean
"""
if lsc_avlb:
diff = list((np.array(lsc[idx]) - np.array(lsc[idx-1])) /
np.array(lsc[idx-1]))
diff = map(abs, diff)
max_abs_diff = max(diff)
if max_abs_diff > LSC_TOL:
print ' max abs(LSC) change:', round(max_abs_diff, 4)
return False
else:
return True
else:
return True
def tonemap_unchanged(raw_cap, tonemap_g, idx):
"""Determine if tonemap unchanged.
Args:
raw_cap: bool; True if RAW capture
tonemap_g: list; green tonemap
idx: int; frame index
Returns:
boolean
"""
if not raw_cap:
return tonemap_g[idx-1] == tonemap_g[idx]
else:
return True
def is_awb_af_stable(cap_info, i):
awb_gains_0 = cap_info[i-1]['awb_gains']
awb_gains_1 = cap_info[i]['awb_gains']
ccm_0 = cap_info[i-1]['ccm']
ccm_1 = cap_info[i]['ccm']
fd_0 = cap_info[i-1]['fd']
fd_1 = cap_info[i]['fd']
return (np.allclose(awb_gains_0, awb_gains_1, rtol=0.01) and
ccm_0 == ccm_1 and np.isclose(fd_0, fd_1, rtol=0.01))
def main():
"""Tests PER_FRAME_CONTROL properties for auto capture requests.
If debug is required, MANUAL_POSTPROCESSING capability is implied
since its.caps.read_3a is valid for test. Debug can 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}
"""
with its.device.ItsSession() as cam:
props = cam.get_camera_properties()
its.caps.skip_unless(its.caps.per_frame_control(props) and
its.caps.read_3a(props))
debug = its.caps.debug_mode()
raw_avlb = its.caps.raw16(props)
largest_yuv = its.objects.get_largest_yuv_format(props)
match_ar = (largest_yuv['width'], largest_yuv['height'])
fmts = [its.objects.get_smallest_yuv_format(props, match_ar=match_ar)]
if raw_avlb and debug:
fmts.insert(0, cam.CAP_RAW)
failed = []
for f, fmt in enumerate(fmts):
print 'fmt:', fmt['format']
cam.do_3a()
req = its.objects.auto_capture_request()
cap_info = {}
ae_states = []
lumas = []
total_gains = []
tonemap_g = []
lsc = []
num_caps = NUM_CAPS
num_frames = NUM_FRAMES
raw_cap = f == 0 and raw_avlb and debug
lsc_avlb = its.caps.lsc_map(props) and not raw_cap
print 'lens shading correction available:', lsc_avlb
if lsc_avlb:
req['android.statistics.lensShadingMapMode'] = 1
name_suffix = 'YUV'
if raw_cap:
name_suffix = 'RAW'
# break up caps if RAW to reduce load
num_caps = NUM_CAPS * 6
num_frames = NUM_FRAMES / 6
for j in range(num_caps):
caps = cam.do_capture([req]*num_frames, fmt)
for i, cap in enumerate(caps):
frame = {}
idx = i + j * num_frames
print '=========== frame %d ==========' % idx
# RAW --> GR, YUV --> Y plane
if raw_cap:
plane = its.image.convert_capture_to_planes(
cap, props=props)[1]
else:
plane = its.image.convert_capture_to_planes(cap)[0]
tile = its.image.get_image_patch(
plane, 0.45, 0.45, 0.1, 0.1)
luma = its.image.compute_image_means(tile)[0]
ae_state = cap['metadata']['android.control.aeState']
iso = cap['metadata']['android.sensor.sensitivity']
isp_gain = cap['metadata']['android.control.postRawSensitivityBoost']
exp_time = cap['metadata']['android.sensor.exposureTime']
total_gain = iso * isp_gain / 100.0 * exp_time / 1000000.0
if raw_cap:
total_gain = iso * exp_time / 1000000.0
awb_state = cap['metadata']['android.control.awbState']
frame['awb_gains'] = cap['metadata']['android.colorCorrection.gains']
frame['ccm'] = cap['metadata']['android.colorCorrection.transform']
frame['fd'] = cap['metadata']['android.lens.focusDistance']
# Convert CCM from rational to float, as numpy arrays.
awb_ccm = np.array(its.objects.rational_to_float(frame['ccm'])).reshape(3, 3)
print 'AE: %d ISO: %d ISP_sen: %d exp(ns): %d tot_gain: %f' % (
ae_state, iso, isp_gain, exp_time, total_gain),
print 'luma: %f' % luma
print 'fd: %f' % frame['fd']
print 'AWB state: %d, AWB gains: %s\n AWB matrix: %s' % (
awb_state, str(frame['awb_gains']),
str(awb_ccm))
if not raw_cap:
tonemap = cap['metadata']['android.tonemap.curve']
tonemap_g.append(tonemap['green'])
print 'G tonemap curve:', tonemap_g[idx]
if lsc_avlb:
lsc.append(cap['metadata']['android.statistics.lensShadingCorrectionMap']['map'])
img = its.image.convert_capture_to_rgb_image(
cap, props=props)
its.image.write_image(img, '%s_frame_%s_%d.jpg' % (
NAME, name_suffix, idx))
cap_info[idx] = frame
ae_states.append(ae_state)
lumas.append(luma)
total_gains.append(total_gain)
norm_gains = [x/max(total_gains)*max(lumas) for x in total_gains]
pylab.figure(name_suffix)
pylab.plot(range(len(lumas)), lumas, '-g.',
label='Center patch brightness')
pylab.plot(range(len(norm_gains)), norm_gains, '-r.',
label='Metadata AE setting product')
pylab.title(NAME + ' ' + name_suffix)
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:
ymin = round(yavg - 1.5*DELTA_LUMA_THRESH, 3)
ymax = round(yavg + 1.5*DELTA_LUMA_THRESH, 3)
pylab.ylim(ymin, ymax)
pylab.legend()
matplotlib.pyplot.savefig('%s_plot_%s.png' % (NAME, name_suffix))
print '\nfmt:', fmt['format']
for i in range(1, num_caps*num_frames):
if is_awb_af_stable(cap_info, i):
prev_total_gain = total_gains[i-1]
total_gain = total_gains[i]
delta_gain = total_gain - prev_total_gain
prev_luma = lumas[i-1]
luma = lumas[i]
delta_luma = luma - prev_luma
delta_gain_rel = delta_gain / prev_total_gain
delta_luma_rel = delta_luma / prev_luma
# luma and total_gain should change in same direction
msg = '%s: frame %d: gain %.1f -> %.1f (%.1f%%), ' % (
fmt['format'], i, prev_total_gain, total_gain,
delta_gain_rel*100)
msg += 'luma %f -> %f (%.2f%%) GAIN/LUMA OPPOSITE DIR' % (
prev_luma, luma, delta_luma_rel*100)
# Threshold change to trigger check. Small delta_gain might
# not be enough to generate a reliable delta_luma to
# overcome frame-to-frame variation.
if (tonemap_unchanged(raw_cap, tonemap_g, i) and
lsc_unchanged(lsc_avlb, lsc, i)):
if abs(delta_gain_rel) > DELTA_GAIN_THRESH:
print ' frame %d: %.2f%% delta gain,' % (
i, delta_gain_rel*100),
print '%.2f%% delta luma' % (delta_luma_rel*100)
if delta_gain * delta_luma < 0.0:
failed.append(msg)
elif abs(delta_gain_rel) < DELTA_NO_GAIN_THRESH:
print ' frame %d: <|%.1f%%| delta gain,' % (
i, DELTA_NO_GAIN_THRESH*100),
print '%.2f%% delta luma' % (delta_luma_rel*100)
msg = '%s: ' % fmt['format']
msg += 'frame %d: gain %.1f -> %.1f (%.1f%%), ' % (
i, prev_total_gain, total_gain,
delta_gain_rel*100)
msg += 'luma %f -> %f (%.1f%%) ' % (
prev_luma, luma, delta_luma_rel*100)
msg += '<|%.1f%%| GAIN, >|%.f%%| LUMA DELTA' % (
DELTA_NO_GAIN_THRESH*100, DELTA_LUMA_THRESH*100)
if abs(delta_luma_rel) > DELTA_LUMA_THRESH:
failed.append(msg)
else:
print ' frame %d: %.1f%% delta gain,' % (
i, delta_gain_rel*100),
print '%.2f%% delta luma' % (delta_luma_rel*100)
else:
print ' frame %d -> %d: tonemap' % (i-1, i),
print 'or lens shading correction changed'
else:
print ' frame %d -> %d: AWB/AF changed' % (i-1, i)
for i in range(len(lumas)):
luma = lumas[i]
ae_state = ae_states[i]
if (ae_state == AE_STATE_CONVERGED or
ae_state == AE_STATE_FLASH_REQUIRED):
msg = '%s: frame %d AE converged ' % (fmt['format'], i)
msg += 'luma %f. valid range: (%f, %f)' % (
luma, VALID_STABLE_LUMA_MIN, VALID_STABLE_LUMA_MAX)
if VALID_STABLE_LUMA_MIN > luma > VALID_STABLE_LUMA_MAX:
failed.append(msg)
if failed:
print '\nError summary'
for fail in failed:
print fail
assert not failed
if __name__ == '__main__':
main()