• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""Verifies changing AE/AWB regions changes images AE/AWB results."""
15
16
17import logging
18import math
19import os.path
20
21from mobly import test_runner
22import numpy
23
24import camera_properties_utils
25import capture_request_utils
26import image_processing_utils
27import its_base_test
28import its_session_utils
29import opencv_processing_utils
30import video_processing_utils
31
32_AE_CHANGE_THRESH = 1  # Incorrect behavior is empirically < 0.5 percent
33_AWB_CHANGE_THRESH = 2  # Incorrect behavior is empirically < 1.5 percent
34_AE_AWB_METER_WEIGHT = 1000  # 1 - 1000 with 1000 as the highest
35_ARUCO_MARKERS_COUNT = 4
36_AE_AWB_REGIONS_AVAILABLE = 1  # Valid range is >= 0, and unavailable if 0
37_IMG_FORMAT = 'png'
38_MIRRORED_PREVIEW_SENSOR_ORIENTATIONS = (0, 180)
39_NAME = os.path.splitext(os.path.basename(__file__))[0]
40_NUM_AE_AWB_REGIONS = 4
41_NUM_FRAMES = 4
42_PERCENTAGE = 100
43_REGION_DURATION_MS = 1800  # 1.8 seconds
44
45
46def _do_ae_check(light, dark, file_name_with_path):
47  """Checks luma change between two images is above threshold.
48
49  Checks that the Y-average of image with darker metering region
50  is higher than the Y-average of image with lighter metering
51  region. Y stands for brightness, or "luma".
52
53  Args:
54    light: RGB image; metering light region.
55    dark: RGB image; metering dark region.
56    file_name_with_path: str; path to preview recording.
57  """
58  # Converts img to YUV and returns Y-average
59  light_y = opencv_processing_utils.convert_to_y(light, 'RGB')
60  light_y_avg = numpy.average(light_y)
61  dark_y = opencv_processing_utils.convert_to_y(dark, 'RGB')
62  dark_y_avg = numpy.average(dark_y)
63  logging.debug('Light image Y-average: %.4f', light_y_avg)
64  logging.debug('Dark image Y-average: %.4f', dark_y_avg)
65  # Checks average change in Y-average between two images
66  y_avg_change = (
67      (dark_y_avg-light_y_avg)/light_y_avg)*_PERCENTAGE
68  logging.debug('Y-average percentage change: %.4f', y_avg_change)
69
70  # Don't change print to logging. Used for KPI.
71  print(f'{_NAME}_ae_y_change: ', y_avg_change)
72
73  if y_avg_change < _AE_CHANGE_THRESH:
74    raise AssertionError(
75        f'Luma change {y_avg_change} is less than the threshold: '
76        f'{_AE_CHANGE_THRESH}')
77  else:
78    its_session_utils.remove_mp4_file(file_name_with_path)
79
80
81def _do_awb_check(blue, yellow):
82  """Checks the ratio of red over blue between two RGB images.
83
84  Checks that the R/B of image with blue metering region
85  is higher than the R/B of image with yellow metering
86  region.
87
88  Args:
89    blue: RGB image; metering blue region.
90    yellow: RGB image; metering yellow region.
91  Returns:
92    failure_messages: (list of strings) of error messages.
93  """
94  # Calculates average red value over average blue value in images
95  blue_r_b_ratio = _get_red_blue_ratio(blue)
96  yellow_r_b_ratio = _get_red_blue_ratio(yellow)
97  logging.debug('Blue image R/B ratio: %s', blue_r_b_ratio)
98  logging.debug('Yellow image R/B ratio: %s', yellow_r_b_ratio)
99  # Calculates change in red over blue values between two images
100  r_b_ratio_change = (
101      (blue_r_b_ratio-yellow_r_b_ratio)/yellow_r_b_ratio)*_PERCENTAGE
102  logging.debug('R/B ratio change in percentage: %.4f', r_b_ratio_change)
103
104  # Don't change print to logging. Used for KPI.
105  print(f'{_NAME}_awb_rb_change: ', r_b_ratio_change)
106
107  if r_b_ratio_change < _AWB_CHANGE_THRESH:
108    raise AssertionError(
109        f'R/B ratio change {r_b_ratio_change} is less than the'
110        f' threshold: {_AWB_CHANGE_THRESH}')
111
112
113def _extract_and_process_select_frames_from_recording(
114    log_path, file_name, video_fps):
115  """Extract key frames (1 frame per 2 seconds) from recordings.
116
117  Args:
118    log_path: str; file location.
119    file_name: str; file name for saved video.
120    video_fps: str; numerical value of supported video fps.
121  Returns:
122    dictionary of images.
123  """
124  # TODO: b/330382627 - Add function to preview_processing_utils
125  # Extract key frames from video
126  frames = video_processing_utils.extract_all_frames_from_video(
127      log_path, file_name, _IMG_FORMAT, video_fps)
128  logging.debug('Number of frames %d', len(frames))
129  # Minus one from interval to avoid going out of bounds
130  interval = math.floor(len(frames) / _NUM_FRAMES) - 1
131  logging.debug('Interval %d', interval)
132
133  # Process select frame files
134  select_frames = []
135  save_files = [os.path.join(log_path, file_name)]
136  for i, frame in enumerate(frames):
137    frame_path = os.path.join(log_path, frame)
138    if (i % interval == 0) and (i > 0):
139      select_frames.append(
140          image_processing_utils.convert_image_to_numpy_array(frame_path))
141      save_files.append(frame_path)
142    else:
143      continue
144  logging.debug('Frame size %d x %d', select_frames[0].shape[1],
145                select_frames[0].shape[0])
146  logging.debug('Number of select frames %d', len(select_frames))
147  its_session_utils.remove_frame_files(log_path, save_files)
148  return select_frames
149
150
151def _get_red_blue_ratio(img):
152  """Computes the ratios of average red over blue in img.
153
154  Args:
155    img: numpy array; RGB image.
156  Returns:
157    r_b_ratio: float; ratio of R and B channel means.
158  """
159  img_means = image_processing_utils.compute_image_means(img)
160  r_b_ratio = img_means[0]/img_means[2]
161  return r_b_ratio
162
163
164class AeAwbRegions(its_base_test.ItsBaseTest):
165  """Tests that changing AE and AWB regions changes image's RGB values.
166
167  Test records an 8 seconds preview recording, and meters a different
168  AE/AWB region (blue, light, dark, yellow) for every 2 seconds.
169  Extracts a frame from each second of recording with a total of 8 frames
170  (2 from each region). For AE check, a frame from light is compared to the
171  dark region. For AWB check, a frame from blue is compared to the yellow
172  region.
173
174  """
175
176  def test_ae_awb_regions(self):
177    """Test AE and AWB regions."""
178
179    with its_session_utils.ItsSession(
180        device_id=self.dut.serial,
181        camera_id=self.camera_id,
182        hidden_physical_id=self.hidden_physical_id) as cam:
183      props = cam.get_camera_properties()
184      props = cam.override_with_hidden_physical_camera_props(props)
185      log_path = self.log_path
186      test_name_with_log_path = os.path.join(log_path, _NAME)
187
188      # Check skip conditions
189      max_ae_regions = props['android.control.maxRegionsAe']
190      max_awb_regions = props['android.control.maxRegionsAwb']
191      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
192      camera_properties_utils.skip_unless(
193          first_api_level >= its_session_utils.ANDROID15_API_LEVEL and
194          camera_properties_utils.ae_regions(props) and
195          (max_awb_regions >= _AE_AWB_REGIONS_AVAILABLE or
196           max_ae_regions >= _AE_AWB_REGIONS_AVAILABLE))
197      logging.debug('maximum AE regions: %d', max_ae_regions)
198      logging.debug('maximum AWB regions: %d', max_awb_regions)
199
200      # Load chart for scene
201      its_session_utils.load_scene(
202          cam, props, self.scene, self.tablet, self.chart_distance,
203          log_path)
204
205      # Find largest preview size to define capture size to find aruco markers
206      common_preview_size_info = (
207          video_processing_utils.get_preview_video_sizes_union(
208              cam, self.camera_id))
209      preview_size = common_preview_size_info.largest_size
210      width = int(preview_size.split('x')[0])
211      height = int(preview_size.split('x')[1])
212      req = capture_request_utils.auto_capture_request()
213      fmt = {'format': 'yuv', 'width': width, 'height': height}
214      cam.do_3a()
215      cap = cam.do_capture(req, fmt)
216
217      # Save image and convert to numpy array
218      img = image_processing_utils.convert_capture_to_rgb_image(
219          cap, props=props)
220      img_path = f'{test_name_with_log_path}_aruco_markers.jpg'
221      image_processing_utils.write_image(img, img_path)
222      img = image_processing_utils.convert_image_to_uint8(img)
223
224      # Define AE/AWB metering regions
225      chart_path = f'{test_name_with_log_path}_chart_boundary.jpg'
226      ae_awb_regions = opencv_processing_utils.define_regions(
227          img, img_path, chart_path, props, width, height)
228
229      # Do preview recording with pre-defined AE/AWB regions
230      recording_obj = cam.do_preview_recording_with_dynamic_ae_awb_region(
231          preview_size, ae_awb_regions, _REGION_DURATION_MS)
232      logging.debug('Tested quality: %s', recording_obj['quality'])
233
234      # Grab the video from the save location on DUT
235      self.dut.adb.pull([recording_obj['recordedOutputPath'], log_path])
236      file_name = recording_obj['recordedOutputPath'].split('/')[-1]
237      file_name_with_path = os.path.join(log_path, file_name)
238      logging.debug('file_name: %s', file_name)
239
240      # Determine acceptable ranges
241      fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props)
242      ae_target_fps_range = camera_properties_utils.get_fps_range_to_test(
243          fps_ranges)
244      video_fps = str(ae_target_fps_range[0])
245
246      # Extract 1 frames per 2 seconds of preview recording
247      # Meters each region of 4 (blue, light, dark, yellow) for 2 seconds
248      # Unpack frames based on metering region's color
249      # If testing front camera with preview mirrored, reverse order.
250      # pylint: disable=unbalanced-tuple-unpacking
251      if ((props['android.lens.facing'] ==
252           camera_properties_utils.LENS_FACING['FRONT']) and
253          props['android.sensor.orientation'] in
254          _MIRRORED_PREVIEW_SENSOR_ORIENTATIONS):
255        yellow, dark, light, blue = (
256            _extract_and_process_select_frames_from_recording(
257                log_path, file_name, video_fps))
258      else:
259        blue, light, dark, yellow = (
260            _extract_and_process_select_frames_from_recording(
261                log_path, file_name, video_fps))
262
263      # AWB Check : Verify R/B ratio change is greater than threshold
264      if max_awb_regions >= _AE_AWB_REGIONS_AVAILABLE:
265        _do_awb_check(blue, yellow)
266
267      # AE Check: Extract the Y component from rectangle patch
268      if max_ae_regions >= _AE_AWB_REGIONS_AVAILABLE:
269        _do_ae_check(light, dark, file_name_with_path)
270
271if __name__ == '__main__':
272  test_runner.main()
273