blob: 5f84931cc99b0ff769de35bc6eaa3b8de932b727 [file] [log] [blame]
Clemenz Portmanne0ea0872022-02-23 18:24:14 -08001# Copyright 2022 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"""Image Field-of-View utilities for aspect ratio, crop, and FoV tests."""
15
16
17import logging
18import math
Clemenz Portmanne0ea0872022-02-23 18:24:14 -080019
20import cv2
Clemenz Portmannb9470412024-06-11 08:40:33 -070021import numpy as np
22
Clemenz Portmanne0ea0872022-02-23 18:24:14 -080023import camera_properties_utils
24import capture_request_utils
25import image_processing_utils
26import opencv_processing_utils
27
28CIRCLE_COLOR = 0 # [0: black, 255: white]
29CIRCLE_MIN_AREA = 0.01 # 1% of image size
Clemenz Portmann1ad78492022-03-29 15:48:01 -070030FOV_PERCENT_RTOL = 0.15 # Relative tolerance on circle FoV % to expected.
Clemenz Portmanne0ea0872022-02-23 18:24:14 -080031LARGE_SIZE_IMAGE = 2000 # Size of a large image (compared against max(w, h))
Leslie Shaw0704d8c2025-01-29 19:03:27 -080032MIN_FOV = 10
33MAX_FOV = 130
Clemenz Portmanne0ea0872022-02-23 18:24:14 -080034THRESH_AR_L = 0.02 # Aspect ratio test threshold of large images
35THRESH_AR_S = 0.075 # Aspect ratio test threshold of mini images
36THRESH_CROP_L = 0.02 # Crop test threshold of large images
37THRESH_CROP_S = 0.075 # Crop test threshold of mini images
38THRESH_MIN_PIXEL = 4 # Crop test allowed offset
39
40
Leslie Shaw0704d8c2025-01-29 19:03:27 -080041def calc_camera_fov_from_metadata(metadata, props):
42 """Get field of view of camera.
43
44 Args:
45 metadata: capture result's metadata.
46 props: obj; camera properties object.
47 Returns:
48 fov: int; field of view of camera.
49 """
50 sensor_size = props['android.sensor.info.physicalSize']
51 diag = math.sqrt(sensor_size['height']**2 + sensor_size['width']**2)
52 fl = metadata['android.lens.focalLength']
53 logging.debug('Focal length: %.3f', fl)
54 fov = 2 * math.degrees(math.atan(diag / (2 * fl)))
55 logging.debug('Field of view: %.1f degrees', fov)
56
57 if not MIN_FOV <= fov <= MAX_FOV:
58 raise AssertionError(f'FoV error: {fov:.1f}')
59 return fov
60
leslieshawe711d052023-12-07 16:52:30 -080061def calc_scaler_crop_region_ratio(scaler_crop_region, props):
62 """Calculate ratio of scaler crop region area over active array area.
63
64 Args:
65 scaler_crop_region: Rect(left, top, right, bottom)
66 props: camera properties
67
68 Returns:
69 ratio of scaler crop region area over active array area
70 """
71 a = props['android.sensor.info.activeArraySize']
72 s = scaler_crop_region
73 logging.debug('Active array size: %s', a)
74 active_array_area = (a['right'] - a['left']) * (a['bottom'] - a['top'])
75 scaler_crop_region_area = (s['right'] - s['left']) * (s['bottom'] - s['top'])
76 crop_region_active_array_ratio = scaler_crop_region_area / active_array_area
77 return crop_region_active_array_ratio
78
79
Clemenz Portmann1ad78492022-03-29 15:48:01 -070080def check_fov(circle, ref_fov, w, h):
81 """Check the FoV for correct size."""
82 fov_percent = calc_circle_image_ratio(circle['r'], w, h)
83 chk_percent = calc_expected_circle_image_ratio(ref_fov, w, h)
84 if not math.isclose(fov_percent, chk_percent, rel_tol=FOV_PERCENT_RTOL):
85 e_msg = (f'FoV %: {fov_percent:.2f}, Ref FoV %: {chk_percent:.2f}, '
86 f'TOL={FOV_PERCENT_RTOL*100}%, img: {w}x{h}, ref: '
87 f"{ref_fov['w']}x{ref_fov['h']}")
88 return e_msg
89
90
Clemenz Portmanne0ea0872022-02-23 18:24:14 -080091def check_ar(circle, ar_gt, w, h, e_msg_stem):
92 """Check the aspect ratio of the circle.
93
94 size is the larger of w or h.
95 if size >= LARGE_SIZE_IMAGE: use THRESH_AR_L
96 elif size == 0 (extreme case): THRESH_AR_S
97 elif 0 < image size < LARGE_SIZE_IMAGE: scale between THRESH_AR_S & AR_L
98
99 Args:
100 circle: dict with circle parameters
101 ar_gt: aspect ratio ground truth to compare against
102 w: width of image
103 h: height of image
104 e_msg_stem: customized string for error message
105
106 Returns:
107 error string if check fails
108 """
109 thresh_ar = max(THRESH_AR_L, THRESH_AR_S +
110 max(w, h) * (THRESH_AR_L-THRESH_AR_S) / LARGE_SIZE_IMAGE)
111 ar = circle['w'] / circle['h']
112 if not math.isclose(ar, ar_gt, abs_tol=thresh_ar):
113 e_msg = (f'{e_msg_stem} {w}x{h}: aspect_ratio {ar:.3f}, '
114 f'thresh {thresh_ar:.3f}')
115 return e_msg
116
117
118def check_crop(circle, cc_gt, w, h, e_msg_stem, crop_thresh_factor):
119 """Check cropping.
120
121 if size >= LARGE_SIZE_IMAGE: use thresh_crop_l
122 elif size == 0 (extreme case): thresh_crop_s
123 elif 0 < size < LARGE_SIZE_IMAGE: scale between thresh_crop_s & thresh_crop_l
124 Also allow at least THRESH_MIN_PIXEL to prevent threshold being too tight
125 for very small circle.
126
127 Args:
128 circle: dict of circle values
129 cc_gt: circle center {'hori', 'vert'} ground truth (ref'd to img center)
130 w: width of image
131 h: height of image
132 e_msg_stem: text to customize error message
133 crop_thresh_factor: scaling factor for crop thresholds
134
135 Returns:
136 error string if check fails
137 """
138 thresh_crop_l = THRESH_CROP_L * crop_thresh_factor
139 thresh_crop_s = THRESH_CROP_S * crop_thresh_factor
140 thresh_crop_hori = max(
141 [thresh_crop_l,
142 thresh_crop_s + w * (thresh_crop_l - thresh_crop_s) / LARGE_SIZE_IMAGE,
143 THRESH_MIN_PIXEL / circle['w']])
144 thresh_crop_vert = max(
145 [thresh_crop_l,
146 thresh_crop_s + h * (thresh_crop_l - thresh_crop_s) / LARGE_SIZE_IMAGE,
147 THRESH_MIN_PIXEL / circle['h']])
148
149 if (not math.isclose(circle['x_offset'], cc_gt['hori'],
150 abs_tol=thresh_crop_hori) or
151 not math.isclose(circle['y_offset'], cc_gt['vert'],
152 abs_tol=thresh_crop_vert)):
153 valid_x_range = (cc_gt['hori'] - thresh_crop_hori,
154 cc_gt['hori'] + thresh_crop_hori)
155 valid_y_range = (cc_gt['vert'] - thresh_crop_vert,
156 cc_gt['vert'] + thresh_crop_vert)
157 e_msg = (f'{e_msg_stem} {w}x{h} '
158 f"offset X {circle['x_offset']:.3f}, Y {circle['y_offset']:.3f}, "
159 f'valid X range: {valid_x_range[0]:.3f} ~ {valid_x_range[1]:.3f}, '
160 f'valid Y range: {valid_y_range[0]:.3f} ~ {valid_y_range[1]:.3f}')
161 return e_msg
162
163
164def calc_expected_circle_image_ratio(ref_fov, img_w, img_h):
165 """Determine the circle image area ratio in percentage for a given image size.
166
167 Cropping happens either horizontally or vertically. In both cases crop results
168 in the visble area reduced by a ratio r (r < 1) and the circle will in turn
169 occupy ref_pct/r (percent) on the target image size.
170
171 Args:
172 ref_fov: dict with {fmt, % coverage, w, h, circle_w, circle_h}
173 img_w: the image width
174 img_h: the image height
175
176 Returns:
177 chk_percent: the expected circle image area ratio in percentage
178 """
179 ar_ref = ref_fov['w'] / ref_fov['h']
180 ar_target = img_w / img_h
181
182 r = ar_ref / ar_target
183 if r < 1.0:
184 r = 1.0 / r
185 return ref_fov['percent'] * r
186
187
188def calc_circle_image_ratio(radius, img_w, img_h):
189 """Calculate the percent of area the input circle covers in input image.
190
191 Args:
192 radius: radius of circle
193 img_w: int width of image
194 img_h: int height of image
195 Returns:
196 fov_percent: float % of image covered by circle
197 """
198 return 100 * math.pi * math.pow(radius, 2) / (img_w * img_h)
199
200
Clemenz Portmann66bb0052022-03-02 13:34:30 -0800201def find_fov_reference(cam, req, props, raw_bool, ref_img_name_stem):
Clemenz Portmanne0ea0872022-02-23 18:24:14 -0800202 """Determine the circle coverage of the image in reference image.
203
204 Captures a full-frame RAW or JPEG and uses its aspect ratio and circle center
205 location as ground truth for the other jpeg or yuv images.
206
207 The intrinsics and distortion coefficients are meant for full-sized RAW,
208 so convert_capture_to_rgb_image returns a 2x downsampled version, so resizes
209 RGB back to full size.
210
211 If the device supports lens distortion correction, applies the coefficients on
212 the RAW image so it can be compared to YUV/JPEG outputs which are subject
213 to the same correction via ISP.
214
215 Finds circle size and location for reference values in calculations for other
216 formats.
217
218 Args:
219 cam: camera object
220 req: camera request
221 props: camera properties
Clemenz Portmann66bb0052022-03-02 13:34:30 -0800222 raw_bool: True if RAW available
Clemenz Portmanne0ea0872022-02-23 18:24:14 -0800223 ref_img_name_stem: test _NAME + location to save data
224
225 Returns:
Clemenz Portmannb9899f42022-11-14 12:52:59 -0800226 ref_fov: dict with {fmt, % coverage, w, h, circle_w, circle_h}
Clemenz Portmanne0ea0872022-02-23 18:24:14 -0800227 cc_ct_gt: circle center position relative to the center of image.
228 aspect_ratio_gt: aspect ratio of the detected circle in float.
229 """
Clemenz Portmann66bb0052022-03-02 13:34:30 -0800230 logging.debug('Creating references for fov_coverage')
231 if raw_bool:
232 logging.debug('Using RAW for reference')
233 fmt_type = 'RAW'
Clemenz Portmanne0ea0872022-02-23 18:24:14 -0800234 out_surface = {'format': 'raw'}
235 cap = cam.do_capture(req, out_surface)
236 logging.debug('Captured RAW %dx%d', cap['width'], cap['height'])
237 img = image_processing_utils.convert_capture_to_rgb_image(
238 cap, props=props)
239 # Resize back up to full scale.
240 img = cv2.resize(img, (0, 0), fx=2.0, fy=2.0)
241
Clemenz Portmannb9470412024-06-11 08:40:33 -0700242 fd = float(cap['metadata']['android.lens.focalLength'])
243 k = camera_properties_utils.get_intrinsic_calibration(
244 props, cap['metadata'], True, fd
245 )
Clemenz Portmanne0ea0872022-02-23 18:24:14 -0800246 if (camera_properties_utils.distortion_correction(props) and
Clemenz Portmannb9470412024-06-11 08:40:33 -0700247 isinstance(k, np.ndarray)):
Clemenz Portmanne0ea0872022-02-23 18:24:14 -0800248 logging.debug('Applying intrinsic calibration and distortion params')
Clemenz Portmanne0ea0872022-02-23 18:24:14 -0800249 opencv_dist = camera_properties_utils.get_distortion_matrix(props)
250 k_new = cv2.getOptimalNewCameraMatrix(
251 k, opencv_dist, (img.shape[1], img.shape[0]), 0)[0]
252 scale = max(k_new[0][0] / k[0][0], k_new[1][1] / k[1][1])
253 if scale > 1:
254 k_new[0][0] = k[0][0] * scale
255 k_new[1][1] = k[1][1] * scale
256 img = cv2.undistort(img, k, opencv_dist, None, k_new)
257 else:
258 img = cv2.undistort(img, k, opencv_dist)
259 size = img.shape
260
Clemenz Portmann66bb0052022-03-02 13:34:30 -0800261 else:
262 logging.debug('Using JPEG for reference')
263 fmt_type = 'JPEG'
Clemenz Portmanne0ea0872022-02-23 18:24:14 -0800264 ref_fov = {}
Clemenz Portmann49f35a42024-07-11 09:28:55 -0700265 fmt = capture_request_utils.get_largest_format('jpeg', props)
Clemenz Portmanne0ea0872022-02-23 18:24:14 -0800266 cap = cam.do_capture(req, fmt)
267 logging.debug('Captured JPEG %dx%d', cap['width'], cap['height'])
268 img = image_processing_utils.convert_capture_to_rgb_image(cap, props)
269 size = (cap['height'], cap['width'])
270
271 # Get image size.
272 w = size[1]
273 h = size[0]
274 img_name = f'{ref_img_name_stem}_{fmt_type}_w{w}_h{h}.png'
275 image_processing_utils.write_image(img, img_name, True)
276
277 # Find circle.
278 img *= 255 # cv2 needs images between [0,255].
279 circle = opencv_processing_utils.find_circle(
280 img, img_name, CIRCLE_MIN_AREA, CIRCLE_COLOR)
281 opencv_processing_utils.append_circle_center_to_img(circle, img, img_name)
282
283 # Determine final return values.
284 if fmt_type == 'RAW':
285 aspect_ratio_gt = circle['w'] / circle['h']
286 else:
287 aspect_ratio_gt = 1.0
288 cc_ct_gt = {'hori': circle['x_offset'], 'vert': circle['y_offset']}
289 fov_percent = calc_circle_image_ratio(circle['r'], w, h)
290 ref_fov = {}
291 ref_fov['fmt'] = fmt_type
292 ref_fov['percent'] = fov_percent
293 ref_fov['w'] = w
294 ref_fov['h'] = h
295 ref_fov['circle_w'] = circle['w']
296 ref_fov['circle_h'] = circle['h']
297 logging.debug('Using %s reference: %s', fmt_type, str(ref_fov))
298 return ref_fov, cc_ct_gt, aspect_ratio_gt