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"""Test the camera in-sensor zoom behavior."""
15
16import logging
17import os.path
18
19import camera_properties_utils
20import capture_request_utils
21import cv2
22import image_processing_utils
23import its_base_test
24import its_session_utils
25import zoom_capture_utils
26
27from mobly import test_runner
28import numpy as np
29
30_NAME = os.path.splitext(os.path.basename(__file__))[0]
31_NUM_STEPS = 10
32_THRESHOLD_MAX_RMS_DIFF_CROPPED_RAW_USE_CASE = 0.06
33
34
35class InSensorZoomTest(its_base_test.ItsBaseTest):
36
37  """Use case CROPPED_RAW: verify that CaptureResult.RAW_CROP_REGION matches cropped RAW image."""
38
39  def test_in_sensor_zoom(self):
40    with its_session_utils.ItsSession(
41        device_id=self.dut.serial,
42        camera_id=self.camera_id,
43        hidden_physical_id=self.hidden_physical_id) as cam:
44      logical_props = cam.get_camera_properties()
45      props = cam.override_with_hidden_physical_camera_props(logical_props)
46      name_with_log_path = os.path.join(self.log_path, _NAME)
47      debug = self.debug_mode
48      # Skip the test if CROPPED_RAW is not present in stream use cases
49      camera_properties_utils.skip_unless(
50          camera_properties_utils.cropped_raw_stream_use_case(props))
51
52      # Load chart for scene
53      its_session_utils.load_scene(
54          cam, props, self.scene, self.tablet, self.chart_distance)
55
56      z_range = props['android.control.zoomRatioRange']
57      logging.debug('In sensor zoom: testing zoomRatioRange: %s', str(z_range))
58
59      z_min, z_max = float(z_range[0]), float(z_range[1])
60      camera_properties_utils.skip_unless(
61          z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH)
62      z_list = np.arange(z_min, z_max, float(z_max - z_min) / (_NUM_STEPS - 1))
63      z_list = np.append(z_list, z_max)
64
65      a = props['android.sensor.info.activeArraySize']
66      aw, ah = a['right'] - a['left'], a['bottom'] - a['top']
67
68      # Capture a RAW frame without any zoom
69      raw_size = capture_request_utils.get_available_output_sizes(
70          'raw', props)[0]
71      output_surfaces = [{'format' : 'raw',
72                          'width': raw_size[0],
73                          'height': raw_size[1]}]
74      if self.hidden_physical_id:
75        output_surfaces[0].update({'physicalCamera' : self.hidden_physical_id})
76      imgs = {}
77      cam.do_3a(out_surfaces=output_surfaces)
78      req = capture_request_utils.auto_capture_request()
79      req['android.statistics.lensShadingMapMode'] = (
80          image_processing_utils.LENS_SHADING_MAP_ON)
81      cap_raw_full = cam.do_capture(
82          req,
83          output_surfaces,
84          reuse_session=True)
85      rgb_full_img = image_processing_utils.convert_raw_capture_to_rgb_image(
86          cap_raw_full, props, 'raw', name_with_log_path)
87      image_processing_utils.write_image(
88          rgb_full_img, f'{name_with_log_path}_raw_full.jpg')
89      imgs['raw_full'] = rgb_full_img
90      output_surfaces[0].update({'useCase' : its_session_utils.USE_CASE_CROPPED_RAW})
91      # Capture RAW images with different zoom ratios with stream use case
92      # CROPPED_RAW set
93      for _, z in enumerate(z_list):
94        req['android.control.zoomRatio'] = z
95        cam.do_3a(out_surfaces=output_surfaces)
96        cap_zoomed_raw = cam.do_capture(
97            req,
98            output_surfaces,
99            reuse_session=True)
100        rgb_zoomed_raw = (
101            image_processing_utils.convert_raw_capture_to_rgb_image(
102                cap_zoomed_raw, props, 'raw', name_with_log_path))
103        # Dump zoomed in RAW image
104        img_name = f'{name_with_log_path}_zoomed_raw_{z:.2f}.jpg'
105        image_processing_utils.write_image(rgb_zoomed_raw, img_name)
106        size_raw = [cap_zoomed_raw['width'], cap_zoomed_raw['height']]
107        logging.debug('Finding center circle for zoom %f: size [%d x %d],'
108                      ' (min zoom %f)', z, cap_zoomed_raw['width'],
109                      cap_zoomed_raw['height'], z_list[0])
110        meta = cap_zoomed_raw['metadata']
111        result_raw_crop_region = meta['android.scaler.rawCropRegion']
112        rl = result_raw_crop_region['left']
113        rt = result_raw_crop_region['top']
114        # Make sure that scale factor for width and height scaling is the same.
115        rw = result_raw_crop_region['right'] - rl
116        rh = result_raw_crop_region['bottom'] - rt
117        logging.debug('RAW_CROP_REGION reported for zoom %f: [%d %d %d %d]',
118                      z, rl, rt, rw, rh)
119        # Effective zoom ratio. May not be == z since its possible the HAL
120        # wasn't able to crop RAW.
121        effective_zoom_ratio = aw / rw
122        logging.debug('Effective zoom ratio: %f', effective_zoom_ratio)
123        inv_scale_factor = rw / aw
124        if aw / rw != ah / rh:
125          raise AssertionError('RAW_CROP_REGION width and height aspect ratio'
126                               f' != active array AR, region size: {rw} x {rh} '
127                               f' active array size: {aw} x {ah}')
128        # Find FoV to determine minimum circle size for
129        # find_center_circle's parameter
130        fov_ratio = zoom_capture_utils._DEFAULT_FOV_RATIO
131        if self.hidden_physical_id is not None:
132          logical_cam_fov = float(cam.calc_camera_fov(logical_props))
133          cam_fov = float(cam.calc_camera_fov(props))
134          logging.debug('Logical camera FoV: %f', logical_cam_fov)
135          logging.debug(
136              'Camera %s under test FoV: %f', self.hidden_physical_id, cam_fov)
137          if cam_fov > logical_cam_fov:
138            fov_ratio = logical_cam_fov / cam_fov
139        # Find the center circle in img
140        circle = zoom_capture_utils.find_center_circle(
141            rgb_zoomed_raw, img_name, size_raw, effective_zoom_ratio,
142            z_list[0], fov_ratio=fov_ratio, debug=True)
143        # Zoom is too large to find center circle, break out
144        if circle is None:
145          break
146
147        xnorm = rl / aw
148        ynorm = rt / ah
149        wnorm = rw / aw
150        hnorm = rh / ah
151        logging.debug('Image patch norm for zoom %.2f: [%.2f %.2f %.2f %.2f]',
152                      z, xnorm, ynorm, wnorm, hnorm)
153        # Crop the full FoV RAW to result_raw_crop_region
154        rgb_full_cropped = image_processing_utils.get_image_patch(
155            rgb_full_img, xnorm, ynorm, wnorm, hnorm)
156
157        # Downscale the zoomed-in RAW image returned by the camera sub-system
158        rgb_zoomed_downscale = cv2.resize(
159            rgb_zoomed_raw, None, fx=inv_scale_factor, fy=inv_scale_factor)
160
161        # Debug dump images being rms compared
162        img_name_downscaled = f'{name_with_log_path}_downscale_raw_{z:.2f}.jpg'
163        image_processing_utils.write_image(
164            rgb_zoomed_downscale, img_name_downscaled)
165
166        img_name_cropped = f'{name_with_log_path}_full_cropped_raw_{z:.2f}.jpg'
167        image_processing_utils.write_image(rgb_full_cropped, img_name_cropped)
168
169        rms_diff = image_processing_utils.compute_image_rms_difference_3d(
170            rgb_zoomed_downscale, rgb_full_cropped)
171        msg = f'RMS diff for CROPPED_RAW use case: {rms_diff:.4f}'
172        logging.debug('%s', msg)
173        if rms_diff >= _THRESHOLD_MAX_RMS_DIFF_CROPPED_RAW_USE_CASE:
174          raise AssertionError('RMS diff of downscaled cropped RAW & full > 1%')
175
176
177if __name__ == '__main__':
178  test_runner.main()
179