1# Copyright 2024 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 zoom ratio scales ArUco marker sizes correctly.""" 15 16import logging 17import os.path 18import subprocess 19 20import cv2 21from mobly import test_runner 22 23import its_base_test 24import camera_properties_utils 25import its_session_utils 26import opencv_processing_utils 27import preview_processing_utils 28import video_processing_utils 29import zoom_capture_utils 30 31 32_CRF = 23 33_CV2_RED = (0, 0, 255) # color (B, G, R) in cv2 to draw lines 34_FPS = 30 35_MP4V = 'mp4v' 36_NAME = os.path.splitext(os.path.basename(__file__))[0] 37_NUM_STEPS = 50 38 39 40def compress_video(input_filename, output_filename, crf=_CRF): 41 """Compresses the given video using ffmpeg.""" 42 43 ffmpeg_cmd = [ 44 'ffmpeg', 45 '-i', input_filename, # Input file 46 '-c:v', 'libx264', # Use H.264 codec 47 '-crf', str(crf), # Set Constant Rate Factor (adjust for quality) 48 '-preset', 'medium', # Encoding speed/compression balance 49 '-c:a', 'copy', # Copy audio stream without re-encoding 50 output_filename # Output file 51 ] 52 53 with open(os.devnull, 'w') as devnull: 54 subprocess.run(ffmpeg_cmd, stdout=devnull, 55 stderr=subprocess.STDOUT, check=False) 56 57 58class PreviewZoomTest(its_base_test.ItsBaseTest): 59 """Verify zoom ratio of preview frames matches values in TotalCaptureResult.""" 60 61 def test_preview_zoom(self): 62 log_path = self.log_path 63 video_processing_utils.log_ffmpeg_version() 64 65 with its_session_utils.ItsSession( 66 device_id=self.dut.serial, 67 camera_id=self.camera_id, 68 hidden_physical_id=self.hidden_physical_id) as cam: 69 70 debug = self.debug_mode 71 72 props = cam.get_camera_properties() 73 props = cam.override_with_hidden_physical_camera_props(props) 74 camera_properties_utils.skip_unless( 75 camera_properties_utils.zoom_ratio_range(props)) 76 77 # Load chart for scene 78 its_session_utils.load_scene( 79 cam, props, self.scene, self.tablet, self.chart_distance) 80 81 # Raise error if not FRONT or REAR facing camera 82 camera_properties_utils.check_front_or_rear_camera(props) 83 84 # set TOLs based on camera and test rig params 85 if camera_properties_utils.logical_multi_camera(props): 86 test_tols, _ = zoom_capture_utils.get_test_tols_and_cap_size( 87 cam, props, self.chart_distance, debug) 88 else: 89 test_tols = {} 90 fls = props['android.lens.info.availableFocalLengths'] 91 for fl in fls: 92 test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL, 93 zoom_capture_utils.OFFSET_RTOL) 94 logging.debug('Threshold levels to be used for testing: %s', test_tols) 95 96 # get max preview size 97 preview_size = preview_processing_utils.get_max_preview_test_size( 98 cam, self.camera_id) 99 size = [int(x) for x in preview_size.split('x')] 100 logging.debug('preview_size = %s', preview_size) 101 logging.debug('size = %s', size) 102 103 # Determine test zoom range and step size 104 z_range = props['android.control.zoomRatioRange'] 105 logging.debug('z_range = %s', str(z_range)) 106 z_min, z_max, z_step_size = zoom_capture_utils.get_preview_zoom_params( 107 z_range, _NUM_STEPS) 108 camera_properties_utils.skip_unless( 109 z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH) 110 111 # recording preview 112 capture_results, file_list = ( 113 preview_processing_utils.preview_over_zoom_range( 114 self.dut, cam, preview_size, z_min, z_max, z_step_size, log_path) 115 ) 116 117 test_data = [] 118 test_data_index = 0 119 all_aruco_ids = [] 120 all_aruco_corners = [] 121 img_paths = [] 122 # Initialize video writer 123 fourcc = cv2.VideoWriter_fourcc(*_MP4V) 124 uncompressed_video = os.path.join(log_path, 125 'output_frames_uncompressed.mp4') 126 out = cv2.VideoWriter(uncompressed_video, fourcc, _FPS, 127 (size[0], size[1])) 128 129 physical_ids = set() 130 for capture_result, img_name in zip(capture_results, file_list): 131 z = float(capture_result['android.control.zoomRatio']) 132 if camera_properties_utils.logical_multi_camera(props): 133 phy_id = capture_result['android.logicalMultiCamera.activePhysicalId'] 134 else: 135 phy_id = None 136 if phy_id: 137 physical_ids.add(phy_id) 138 logging.debug('Physical IDs: %s', physical_ids) 139 140 # read image 141 img_bgr = cv2.imread(os.path.join(log_path, img_name)) 142 143 # add path to image name 144 img_path = f'{os.path.join(self.log_path, img_name)}' 145 146 # determine radius tolerance of capture 147 cap_fl = capture_result['android.lens.focalLength'] 148 radius_tol, offset_tol = test_tols.get( 149 cap_fl, 150 (zoom_capture_utils.RADIUS_RTOL, zoom_capture_utils.OFFSET_RTOL) 151 ) 152 153 # Find ArUco markers 154 # TODO: b/370585114 - improve test time and skip saving debug images. 155 try: 156 corners, ids, _ = opencv_processing_utils.find_aruco_markers( 157 img_bgr, 158 (f'{os.path.join(log_path, img_name)}_{z:.2f}_' 159 f'ArUco.{zoom_capture_utils.JPEG_STR}'), 160 aruco_marker_count=1 161 ) 162 except AssertionError as e: 163 logging.debug('Could not find ArUco marker at zoom ratio %.2f: %s', 164 z, e) 165 break 166 167 all_aruco_corners.append([corner[0] for corner in corners]) 168 all_aruco_ids.append([id[0] for id in ids]) 169 img_paths.append(img_path) 170 171 test_data.append( 172 zoom_capture_utils.ZoomTestData( 173 result_zoom=z, 174 radius_tol=radius_tol, 175 offset_tol=offset_tol, 176 focal_length=cap_fl, 177 physical_id=phy_id 178 ) 179 ) 180 181 logging.debug('test_data[%d] = %s', test_data_index, 182 test_data[test_data_index]) 183 test_data_index = test_data_index + 1 184 185 # Find ArUco markers in all captures and update test data 186 zoom_capture_utils.update_zoom_test_data_with_shared_aruco_marker( 187 test_data, all_aruco_ids, all_aruco_corners, size) 188 # Mark ArUco marker center and image center 189 opencv_processing_utils.mark_zoom_images_to_video( 190 out, img_paths, test_data) 191 192 out.release() 193 # Remove png files 194 for path in img_paths: 195 its_session_utils.remove_file(path) 196 197 # --- Compress Video --- 198 compressed_video = os.path.join(log_path, 'output_frames.mp4') 199 compress_video(uncompressed_video, compressed_video) 200 201 os.remove(uncompressed_video) 202 203 plot_name_stem = f'{os.path.join(log_path, _NAME)}' 204 # TODO: b/369852004 - decrease TOL for test_preview_zoom 205 if not zoom_capture_utils.verify_preview_zoom_results( 206 test_data, size, z_max, z_min, z_step_size, plot_name_stem): 207 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 208 failure_msg = f'{_NAME} failed! Check test_log.DEBUG for errors' 209 if first_api_level >= its_session_utils.ANDROID15_API_LEVEL: 210 raise AssertionError(failure_msg) 211 else: 212 raise AssertionError(f'{its_session_utils.NOT_YET_MANDATED_MESSAGE}' 213 f'\n\n{failure_msg}') 214 215if __name__ == '__main__': 216 test_runner.main() 217