1# Copyright 2013 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Verifies YUV & JPEG image captures have similar brightness.""" 15 16 17import logging 18import os.path 19import matplotlib 20from matplotlib import pylab 21import matplotlib.lines as mlines 22from matplotlib.ticker import MaxNLocator 23from mobly import test_runner 24 25import its_base_test 26import camera_properties_utils 27import capture_request_utils 28import image_processing_utils 29import its_session_utils 30import target_exposure_utils 31 32_JPG_STR = 'jpg' 33_NAME = os.path.splitext(os.path.basename(__file__))[0] 34_PATCH_H = 0.1 # center 10% 35_PATCH_W = 0.1 36_PATCH_X = 0.5 - _PATCH_W/2 37_PATCH_Y = 0.5 - _PATCH_H/2 38_PLOT_ALPHA = 0.5 39_PLOT_MARKER_SIZE = 8 40_PLOT_LEGEND_CIRCLE_SIZE = 10 41_PLOT_LEGEND_TRIANGLE_SIZE = 6 42_THRESHOLD_MAX_RMS_DIFF = 0.03 43_YUV_STR = 'yuv' 44 45 46def do_capture_and_extract_rgb_means( 47 req, cam, props, size, img_type, index, name_with_log_path, debug): 48 """Do capture and extra rgb_means of center patch. 49 50 Args: 51 req: capture request 52 cam: camera object 53 props: camera properties dict 54 size: [width, height] 55 img_type: string of 'yuv' or 'jpeg' 56 index: index to track capture number of img_type 57 name_with_log_path: file name and location for saving image 58 debug: boolean to flag saving captured images 59 60 Returns: 61 center patch RGB means 62 """ 63 out_surface = {'width': size[0], 'height': size[1], 'format': img_type} 64 if camera_properties_utils.stream_use_case(props): 65 out_surface['useCase'] = camera_properties_utils.USE_CASE_STILL_CAPTURE 66 logging.debug('output surface: %s', str(out_surface)) 67 if debug and camera_properties_utils.raw(props): 68 out_surfaces = [{'format': 'raw'}, out_surface] 69 cap_raw, cap = cam.do_capture(req, out_surfaces) 70 img_raw = image_processing_utils.convert_capture_to_rgb_image( 71 cap_raw, props=props) 72 image_processing_utils.write_image( 73 img_raw, 74 f'{name_with_log_path}_raw_{img_type}_w{size[0]}_h{size[1]}.png', True) 75 else: 76 cap = cam.do_capture(req, out_surface) 77 logging.debug('e_cap: %d, s_cap: %d, f_distance: %s', 78 cap['metadata']['android.sensor.exposureTime'], 79 cap['metadata']['android.sensor.sensitivity'], 80 cap['metadata']['android.lens.focusDistance']) 81 if img_type == _JPG_STR: 82 if cap['format'] != 'jpeg': 83 raise AssertionError(f"{cap['format']} != jpeg") 84 img = image_processing_utils.decompress_jpeg_to_rgb_image(cap['data']) 85 else: 86 if cap['format'] != img_type: 87 raise AssertionError(f"{cap['format']} != {img_type}") 88 img = image_processing_utils.convert_capture_to_rgb_image(cap) 89 if cap['width'] != size[0]: 90 raise AssertionError(f"{cap['width']} != {size[0]}") 91 if cap['height'] != size[1]: 92 raise AssertionError(f"{cap['height']} != {size[1]}") 93 94 if debug: 95 image_processing_utils.write_image( 96 img, f'{name_with_log_path}_{img_type}_w{size[0]}_h{size[1]}.png') 97 98 if img_type == _JPG_STR: 99 if img.shape[0] != size[1]: 100 raise AssertionError(f'{img.shape[0]} != {size[1]}') 101 if img.shape[1] != size[0]: 102 raise AssertionError(f'{img.shape[1]} != {size[0]}') 103 if img.shape[2] != 3: 104 raise AssertionError(f'{img.shape[2]} != 3') 105 patch = image_processing_utils.get_image_patch( 106 img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 107 rgb = image_processing_utils.compute_image_means(patch) 108 logging.debug('Captured %s %dx%d rgb = %s, format number = %d', 109 img_type, cap['width'], cap['height'], str(rgb), index) 110 return rgb 111 112 113class YuvJpegAllTest(its_base_test.ItsBaseTest): 114 """Test reported sizes & fmts for YUV & JPEG caps return similar images.""" 115 116 def test_yuv_jpeg_all(self): 117 logging.debug('Starting %s', _NAME) 118 with its_session_utils.ItsSession( 119 device_id=self.dut.serial, 120 camera_id=self.camera_id, 121 hidden_physical_id=self.hidden_physical_id) as cam: 122 props = cam.get_camera_properties() 123 props = cam.override_with_hidden_physical_camera_props(props) 124 125 log_path = self.log_path 126 debug = self.debug_mode 127 name_with_log_path = os.path.join(log_path, _NAME) 128 129 # Check SKIP conditions 130 camera_properties_utils.skip_unless( 131 camera_properties_utils.linear_tonemap(props)) 132 133 # Load chart for scene 134 its_session_utils.load_scene( 135 cam, props, self.scene, self.tablet, 136 its_session_utils.CHART_DISTANCE_NO_SCALING) 137 138 # If device supports target exposure computation, use manual capture. 139 # Otherwise, do 3A, then use an auto request. 140 # Both requests use a linear tonemap and focus distance of 0.0 141 # so that the YUV and JPEG should look the same 142 # (once converted by the image_processing_utils). 143 if camera_properties_utils.compute_target_exposure(props): 144 logging.debug('Using manual capture request') 145 e, s = target_exposure_utils.get_target_exposure_combos( 146 log_path, cam)['midExposureTime'] 147 logging.debug('e_req: %d, s_req: %d', e, s) 148 req = capture_request_utils.manual_capture_request( 149 s, e, 0.0, True, props) 150 match_ar = None 151 else: 152 logging.debug('Using auto capture request') 153 cam.do_3a(do_af=False) 154 req = capture_request_utils.auto_capture_request( 155 linear_tonemap=True, props=props, do_af=False) 156 largest_yuv = capture_request_utils.get_largest_yuv_format(props) 157 match_ar = (largest_yuv['width'], largest_yuv['height']) 158 159 yuv_rgbs = [] 160 for i, size in enumerate( 161 capture_request_utils.get_available_output_sizes( 162 _YUV_STR, props, match_ar_size=match_ar)): 163 yuv_rgbs.append(do_capture_and_extract_rgb_means( 164 req, cam, props, size, _YUV_STR, i, name_with_log_path, debug)) 165 166 jpg_rgbs = [] 167 for i, size in enumerate( 168 capture_request_utils.get_available_output_sizes( 169 _JPG_STR, props, match_ar_size=match_ar)): 170 jpg_rgbs.append(do_capture_and_extract_rgb_means( 171 req, cam, props, size, _JPG_STR, i, name_with_log_path, debug)) 172 173 # Plot means vs format 174 pylab.figure(_NAME) 175 pylab.title(_NAME) 176 yuv_index = range(len(yuv_rgbs)) 177 jpg_index = range(len(jpg_rgbs)) 178 pylab.plot(yuv_index, [rgb[0] for rgb in yuv_rgbs], 179 '-ro', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE) 180 pylab.plot(yuv_index, [rgb[1] for rgb in yuv_rgbs], 181 '-go', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE) 182 pylab.plot(yuv_index, [rgb[2] for rgb in yuv_rgbs], 183 '-bo', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE) 184 pylab.plot(jpg_index, [rgb[0] for rgb in jpg_rgbs], 185 '-r^', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE) 186 pylab.plot(jpg_index, [rgb[1] for rgb in jpg_rgbs], 187 '-g^', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE) 188 pylab.plot(jpg_index, [rgb[2] for rgb in jpg_rgbs], 189 '-b^', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE) 190 pylab.ylim([0, 1]) 191 ax = pylab.gca() 192 # force matplotlib to use integers for x-axis labels 193 ax.xaxis.set_major_locator(MaxNLocator(integer=True)) 194 yuv_marker = mlines.Line2D([], [], linestyle='None', 195 color='black', marker='.', 196 markersize=_PLOT_LEGEND_CIRCLE_SIZE, 197 label='YUV') 198 jpg_marker = mlines.Line2D([], [], linestyle='None', 199 color='black', marker='^', 200 markersize=_PLOT_LEGEND_TRIANGLE_SIZE, 201 label='JPEG') 202 ax.legend(handles=[yuv_marker, jpg_marker]) 203 pylab.xlabel('format number') 204 pylab.ylabel('RGB avg [0, 1]') 205 matplotlib.pyplot.savefig(f'{name_with_log_path}_plot_means.png') 206 207 # Assert all captures are similar in RGB space using rgbs[0] as ref. 208 rgbs = yuv_rgbs + jpg_rgbs 209 max_diff = 0 210 for rgb_i in rgbs[1:]: 211 rms_diff = image_processing_utils.compute_image_rms_difference_1d( 212 rgbs[0], rgb_i) # use first capture as reference 213 max_diff = max(max_diff, rms_diff) 214 msg = f'Max RMS difference: {max_diff:.4f}' 215 logging.debug('%s', msg) 216 if max_diff >= _THRESHOLD_MAX_RMS_DIFF: 217 raise AssertionError(f'{msg} spec: {_THRESHOLD_MAX_RMS_DIFF}') 218 219if __name__ == '__main__': 220 test_runner.main() 221