# 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()