1# Copyright 2023 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"""Verify preview matches video output during video zoom.""" 15 16import logging 17import math 18import os 19 20from mobly import test_runner 21 22import its_base_test 23import camera_properties_utils 24import capture_request_utils 25import image_processing_utils 26import its_session_utils 27import preview_processing_utils 28import video_processing_utils 29import zoom_capture_utils 30 31_CIRCLE_R = 2 32_CIRCLE_X = 0 33_CIRCLE_Y = 1 34_CIRCLISH_RTOL = 0.1 # contour area vs ideal circle area pi*((w+h)/4)**2 35_MAX_STR = 'max' 36_MIN_STR = 'min' 37_MIN_AREA_RATIO = 0.00015 # based on 2000/(4000x3000) pixels 38_MIN_CIRCLE_PTS = 10 39_MIN_RESOLUTION_AREA = 1280*720 # 720P 40_MIN_ZOOM_SCALE_CHART = 0.70 # zoom factor to trigger scaled chart 41_NAME = os.path.splitext(os.path.basename(__file__))[0] 42_OFFSET_TOL = 5 # pixels 43_RADIUS_RTOL = 0.1 # 10% tolerance Video/Preview circle size 44_RECORDING_DURATION = 2 # seconds 45_ZOOM_COMP_MAX_THRESH = 1.15 46_ZOOM_RATIO = 2 47 48 49class PreviewVideoZoomMatchTest(its_base_test.ItsBaseTest): 50 """Tests if preview matches video output when zooming. 51 52 Preview and video are recorded while do_3a() iterate through 53 different cameras with minimal zoom to zoom factor 1.5x. 54 55 The recorded preview and video output are processed to dump all 56 of the frames to PNG files. Camera movement in zoom is extracted 57 from frames by determining if the size of the circle being recorded 58 increases as zoom factor increases. Test is a PASS if both recordings 59 match in zoom factors. 60 """ 61 62 def test_preview_video_zoom_match(self): 63 video_test_data = {} 64 preview_test_data = {} 65 log_path = self.log_path 66 with its_session_utils.ItsSession( 67 device_id=self.dut.serial, 68 camera_id=self.camera_id, 69 hidden_physical_id=self.hidden_physical_id) as cam: 70 props = cam.get_camera_properties() 71 props = cam.override_with_hidden_physical_camera_props(props) 72 debug = self.debug_mode 73 74 def _do_preview_recording(cam, resolution, zoom_ratio): 75 """Record a new set of data from the device. 76 77 Captures camera preview frames while the camera is zooming. 78 79 Args: 80 cam: camera object 81 resolution: str; preview resolution (ex. '1920x1080') 82 zoom_ratio: float; zoom ratio 83 84 Returns: 85 preview recording object as described by cam.do_basic_recording 86 """ 87 88 # Record previews 89 preview_recording_obj = cam.do_preview_recording( 90 resolution, _RECORDING_DURATION, False, zoom_ratio=zoom_ratio) 91 logging.debug('Preview_recording_obj: %s', preview_recording_obj) 92 logging.debug('Recorded output path for preview: %s', 93 preview_recording_obj['recordedOutputPath']) 94 95 # Grab and rename the preview recordings from the save location on DUT 96 self.dut.adb.pull( 97 [preview_recording_obj['recordedOutputPath'], log_path]) 98 preview_file_name = ( 99 preview_recording_obj['recordedOutputPath'].split('/')[-1]) 100 logging.debug('recorded preview name: %s', preview_file_name) 101 102 return preview_file_name 103 104 def _do_video_recording(cam, profile_id, quality, zoom_ratio): 105 """Record a new set of data from the device. 106 107 Captures camera video frames while the camera is zooming per zoom_ratio. 108 109 Args: 110 cam: camera object 111 profile_id: int; profile id corresponding to the quality level 112 quality: str; video recording quality such as High, Low, 480P 113 zoom_ratio: float; zoom ratio. 114 115 Returns: 116 video recording object as described by cam.do_basic_recording 117 """ 118 119 # Record videos 120 video_recording_obj = cam.do_basic_recording( 121 profile_id, quality, _RECORDING_DURATION, 0, zoom_ratio=zoom_ratio) 122 logging.debug('Video_recording_obj: %s', video_recording_obj) 123 logging.debug('Recorded output path for video: %s', 124 video_recording_obj['recordedOutputPath']) 125 126 # Grab and rename the video recordings from the save location on DUT 127 self.dut.adb.pull( 128 [video_recording_obj['recordedOutputPath'], log_path]) 129 video_file_name = ( 130 video_recording_obj['recordedOutputPath'].split('/')[-1]) 131 logging.debug('recorded video name: %s', video_file_name) 132 133 return video_file_name 134 135 # Find zoom range 136 z_range = props['android.control.zoomRatioRange'] 137 138 # Skip unless camera has zoom ability 139 first_api_level = its_session_utils.get_first_api_level( 140 self.dut.serial) 141 camera_properties_utils.skip_unless( 142 z_range and first_api_level >= its_session_utils.ANDROID14_API_LEVEL) 143 logging.debug('Testing zoomRatioRange: %s', z_range) 144 145 # Determine zoom factors 146 z_min = z_range[0] 147 camera_properties_utils.skip_unless( 148 float(z_range[-1]) >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH) 149 zoom_ratios_to_be_tested = [z_min] 150 if z_min < 1.0: 151 zoom_ratios_to_be_tested.append(float(_ZOOM_RATIO)) 152 else: 153 zoom_ratios_to_be_tested.append(float(z_min * 2)) 154 logging.debug('Testing zoom ratios: %s', str(zoom_ratios_to_be_tested)) 155 156 # Load chart for scene 157 if z_min > _MIN_ZOOM_SCALE_CHART: 158 its_session_utils.load_scene( 159 cam, props, self.scene, self.tablet, self.chart_distance) 160 else: # Load full-scale chart for small zoom factor 161 its_session_utils.load_scene( 162 cam, props, self.scene, self.tablet, 163 its_session_utils.CHART_DISTANCE_NO_SCALING) 164 165 # Find supported preview/video sizes, and their smallest and common size 166 supported_preview_sizes = cam.get_supported_preview_sizes(self.camera_id) 167 supported_video_qualities = cam.get_supported_video_qualities( 168 self.camera_id) 169 logging.debug( 170 'Supported video profiles and ID: %s', supported_video_qualities) 171 common_size, common_video_quality = ( 172 video_processing_utils.get_lowest_common_preview_video_size( 173 supported_preview_sizes, supported_video_qualities, 174 _MIN_RESOLUTION_AREA 175 ) 176 ) 177 178 # Start video recording over minZoom and 2x Zoom 179 for quality_profile_id_pair in supported_video_qualities: 180 quality = quality_profile_id_pair.split(':')[0] 181 profile_id = quality_profile_id_pair.split(':')[-1] 182 if quality == common_video_quality: 183 for i, z in enumerate(zoom_ratios_to_be_tested): 184 logging.debug('Testing video recording for quality: %s', quality) 185 req = capture_request_utils.auto_capture_request() 186 req['android.control.zoomRatio'] = z 187 cam.do_3a(zoom_ratio=z) 188 logging.debug('Zoom ratio: %.2f', z) 189 190 # Determine focal length of camera through capture 191 cap = cam.do_capture( 192 req, {'format': 'yuv'}) 193 cap_fl = cap['metadata']['android.lens.focalLength'] 194 logging.debug('Camera focal length: %.2f', cap_fl) 195 196 # Determine width and height of video 197 size = common_size.split('x') 198 width = int(size[0]) 199 height = int(size[1]) 200 201 # Start video recording 202 video_file_name = _do_video_recording( 203 cam, profile_id, quality, zoom_ratio=z) 204 205 # Get key frames from the video recording 206 video_img = ( 207 video_processing_utils.extract_last_key_frame_from_recording( 208 log_path, video_file_name)) 209 210 # Find the center circle in video img 211 img_name_stem = os.path.join(log_path, 'video_zoomRatio') 212 video_img_name = ( 213 f'{img_name_stem}_{z:.2f}_{quality}_circle.png') 214 circle = zoom_capture_utils.find_center_circle( 215 video_img, video_img_name, [width, height], 216 z, z_min, circlish_rtol=_CIRCLISH_RTOL, 217 min_circle_pts=_MIN_CIRCLE_PTS, debug=debug) 218 logging.debug('Recorded video name: %s', video_file_name) 219 220 video_test_data[i] = {'z': z, 'circle': circle} 221 222 # Start preview recording over minZoom and maxZoom 223 for size in supported_preview_sizes: 224 if size == common_size: 225 for i, z in enumerate(zoom_ratios_to_be_tested): 226 cam.do_3a(zoom_ratio=z) 227 preview_file_name = _do_preview_recording( 228 cam, size, zoom_ratio=z) 229 230 # Define width and height from size 231 width = int(size.split('x')[0]) 232 height = int(size.split('x')[1]) 233 234 # Get key frames from the preview recording 235 preview_img = ( 236 video_processing_utils.extract_last_key_frame_from_recording( 237 log_path, preview_file_name)) 238 239 # If front camera, flip preview image to match camera capture 240 if (props['android.lens.facing'] == 241 camera_properties_utils.LENS_FACING['FRONT']): 242 img_name_stem = os.path.join(log_path, 'flipped_preview') 243 img_name = ( 244 f'{img_name_stem}_zoomRatio_{z:.2f}.' 245 f'{zoom_capture_utils.JPEG_STR}') 246 preview_img = ( 247 preview_processing_utils.mirror_preview_image_by_sensor_orientation( 248 props['android.sensor.orientation'], preview_img)) 249 image_processing_utils.write_image(preview_img / 255, img_name) 250 else: 251 img_name_stem = os.path.join(log_path, 'rear_preview') 252 253 # Find the center circle in preview img 254 preview_img_name = ( 255 f'{img_name_stem}_zoomRatio_{z:.2f}_{size}_circle.png') 256 circle = zoom_capture_utils.find_center_circle( 257 preview_img, preview_img_name, [width, height], 258 z, z_min, circlish_rtol=_CIRCLISH_RTOL, 259 min_circle_pts=_MIN_CIRCLE_PTS, debug=debug) 260 261 preview_test_data[i] = {'z': z, 'circle': circle} 262 263 # Compare size and center of preview's circle to video's circle 264 preview_radius = {} 265 video_radius = {} 266 z_idx = {} 267 zoom_factor = {} 268 preview_radius[_MIN_STR] = (preview_test_data[0]['circle'][_CIRCLE_R]) 269 video_radius[_MIN_STR] = (video_test_data[0]['circle'][_CIRCLE_R]) 270 preview_radius[_MAX_STR] = (preview_test_data[1]['circle'][_CIRCLE_R]) 271 video_radius[_MAX_STR] = (video_test_data[1]['circle'][_CIRCLE_R]) 272 z_idx[_MIN_STR] = ( 273 preview_radius[_MIN_STR] / video_radius[_MIN_STR]) 274 z_idx[_MAX_STR] = ( 275 preview_radius[_MAX_STR] / video_radius[_MAX_STR]) 276 z_comparison = z_idx[_MAX_STR] / z_idx[_MIN_STR] 277 zoom_factor[_MIN_STR] = preview_test_data[0]['z'] 278 zoom_factor[_MAX_STR] = preview_test_data[1]['z'] 279 280 # Compare preview circle's center with video circle's center 281 preview_circle_x = preview_test_data[1]['circle'][_CIRCLE_X] 282 video_circle_x = video_test_data[1]['circle'][_CIRCLE_X] 283 preview_circle_y = preview_test_data[1]['circle'][_CIRCLE_Y] 284 video_circle_y = video_test_data[1]['circle'][_CIRCLE_Y] 285 circles_offset_x = math.isclose(preview_circle_x, video_circle_x, 286 abs_tol=_OFFSET_TOL) 287 circles_offset_y = math.isclose(preview_circle_y, video_circle_y, 288 abs_tol=_OFFSET_TOL) 289 logging.debug('Preview circle x: %.2f, Video circle x: %.2f' 290 ' Preview circle y: %.2f, Video circle y: %.2f', 291 preview_circle_x, video_circle_x, 292 preview_circle_y, video_circle_y) 293 logging.debug('Preview circle r: %.2f, Preview circle r zoom: %.2f' 294 ' Video circle r: %.2f, Video circle r zoom: %.2f' 295 ' centers offset x: %s, centers offset y: %s', 296 preview_radius[_MIN_STR], preview_radius[_MAX_STR], 297 video_radius[_MIN_STR], video_radius[_MAX_STR], 298 circles_offset_x, circles_offset_y) 299 if not circles_offset_x or not circles_offset_y: 300 raise AssertionError('Preview and video output do not match! ' 301 'Preview and video circles offset is too great') 302 303 # Check zoom ratio by size of circles before and after zoom 304 for radius_ratio in z_idx.values(): 305 if not math.isclose(radius_ratio, 1, rel_tol=_RADIUS_RTOL): 306 raise AssertionError('Preview and video output do not match! ' 307 f'Radius ratio: {radius_ratio:.2f}') 308 309 if z_comparison > _ZOOM_COMP_MAX_THRESH: 310 raise AssertionError('Preview and video output do not match! ' 311 f'Zoom ratio difference: {z_comparison:.2f}') 312 313if __name__ == '__main__': 314 test_runner.main() 315