• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2013 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 YUV & JPEG image captures have similar brightness."""
15
16
17import logging
18import os.path
19import matplotlib
20from matplotlib import pylab
21import matplotlib.lines as mlines
22from matplotlib.ticker import MaxNLocator
23from mobly import test_runner
24
25import its_base_test
26import camera_properties_utils
27import capture_request_utils
28import image_processing_utils
29import its_session_utils
30import target_exposure_utils
31
32_JPG_STR = 'jpg'
33_NAME = os.path.splitext(os.path.basename(__file__))[0]
34_PATCH_H = 0.1  # center 10%
35_PATCH_W = 0.1
36_PATCH_X = 0.5 - _PATCH_W/2
37_PATCH_Y = 0.5 - _PATCH_H/2
38_PLOT_ALPHA = 0.5
39_PLOT_MARKER_SIZE = 8
40_PLOT_LEGEND_CIRCLE_SIZE = 10
41_PLOT_LEGEND_TRIANGLE_SIZE = 6
42_THRESHOLD_MAX_RMS_DIFF = 0.03
43_YUV_STR = 'yuv'
44
45
46def do_capture_and_extract_rgb_means(
47    req, cam, props, size, img_type, index, name_with_log_path, debug):
48  """Do capture and extra rgb_means of center patch.
49
50  Args:
51    req: capture request
52    cam: camera object
53    props: camera properties dict
54    size: [width, height]
55    img_type: string of 'yuv' or 'jpeg'
56    index: index to track capture number of img_type
57    name_with_log_path: file name and location for saving image
58    debug: boolean to flag saving captured images
59
60  Returns:
61    center patch RGB means
62  """
63  out_surface = {'width': size[0], 'height': size[1], 'format': img_type}
64  if camera_properties_utils.stream_use_case(props):
65    out_surface['useCase'] = camera_properties_utils.USE_CASE_STILL_CAPTURE
66  logging.debug('output surface: %s', str(out_surface))
67  if debug and camera_properties_utils.raw(props):
68    out_surfaces = [{'format': 'raw'}, out_surface]
69    cap_raw, cap = cam.do_capture(req, out_surfaces)
70    img_raw = image_processing_utils.convert_capture_to_rgb_image(
71        cap_raw, props=props)
72    image_processing_utils.write_image(
73        img_raw,
74        f'{name_with_log_path}_raw_{img_type}_w{size[0]}_h{size[1]}.png', True)
75  else:
76    cap = cam.do_capture(req, out_surface)
77  logging.debug('e_cap: %d, s_cap: %d, f_distance: %s',
78                cap['metadata']['android.sensor.exposureTime'],
79                cap['metadata']['android.sensor.sensitivity'],
80                cap['metadata']['android.lens.focusDistance'])
81  if img_type == _JPG_STR:
82    if cap['format'] != 'jpeg':
83      raise AssertionError(f"{cap['format']} != jpeg")
84    img = image_processing_utils.decompress_jpeg_to_rgb_image(cap['data'])
85  else:
86    if cap['format'] != img_type:
87      raise AssertionError(f"{cap['format']} != {img_type}")
88    img = image_processing_utils.convert_capture_to_rgb_image(cap)
89  if cap['width'] != size[0]:
90    raise AssertionError(f"{cap['width']} != {size[0]}")
91  if cap['height'] != size[1]:
92    raise AssertionError(f"{cap['height']} != {size[1]}")
93
94  if debug:
95    image_processing_utils.write_image(
96        img, f'{name_with_log_path}_{img_type}_w{size[0]}_h{size[1]}.png')
97
98  if img_type == _JPG_STR:
99    if img.shape[0] != size[1]:
100      raise AssertionError(f'{img.shape[0]} != {size[1]}')
101    if img.shape[1] != size[0]:
102      raise AssertionError(f'{img.shape[1]} != {size[0]}')
103    if img.shape[2] != 3:
104      raise AssertionError(f'{img.shape[2]} != 3')
105  patch = image_processing_utils.get_image_patch(
106      img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
107  rgb = image_processing_utils.compute_image_means(patch)
108  logging.debug('Captured %s %dx%d rgb = %s, format number = %d',
109                img_type, cap['width'], cap['height'], str(rgb), index)
110  return rgb
111
112
113class YuvJpegAllTest(its_base_test.ItsBaseTest):
114  """Test reported sizes & fmts for YUV & JPEG caps return similar images."""
115
116  def test_yuv_jpeg_all(self):
117    logging.debug('Starting %s', _NAME)
118    with its_session_utils.ItsSession(
119        device_id=self.dut.serial,
120        camera_id=self.camera_id,
121        hidden_physical_id=self.hidden_physical_id) as cam:
122      props = cam.get_camera_properties()
123      props = cam.override_with_hidden_physical_camera_props(props)
124
125      log_path = self.log_path
126      debug = self.debug_mode
127      name_with_log_path = os.path.join(log_path, _NAME)
128
129      # Check SKIP conditions
130      camera_properties_utils.skip_unless(
131          camera_properties_utils.linear_tonemap(props))
132
133      # Load chart for scene
134      its_session_utils.load_scene(
135          cam, props, self.scene, self.tablet,
136          its_session_utils.CHART_DISTANCE_NO_SCALING)
137
138      # If device supports target exposure computation, use manual capture.
139      # Otherwise, do 3A, then use an auto request.
140      # Both requests use a linear tonemap and focus distance of 0.0
141      # so that the YUV and JPEG should look the same
142      # (once converted by the image_processing_utils).
143      if camera_properties_utils.compute_target_exposure(props):
144        logging.debug('Using manual capture request')
145        e, s = target_exposure_utils.get_target_exposure_combos(
146            log_path, cam)['midExposureTime']
147        logging.debug('e_req: %d, s_req: %d', e, s)
148        req = capture_request_utils.manual_capture_request(
149            s, e, 0.0, True, props)
150        match_ar = None
151      else:
152        logging.debug('Using auto capture request')
153        cam.do_3a(do_af=False)
154        req = capture_request_utils.auto_capture_request(
155            linear_tonemap=True, props=props, do_af=False)
156        largest_yuv = capture_request_utils.get_largest_yuv_format(props)
157        match_ar = (largest_yuv['width'], largest_yuv['height'])
158
159      yuv_rgbs = []
160      for i, size in enumerate(
161          capture_request_utils.get_available_output_sizes(
162              _YUV_STR, props, match_ar_size=match_ar)):
163        yuv_rgbs.append(do_capture_and_extract_rgb_means(
164            req, cam, props, size, _YUV_STR, i, name_with_log_path, debug))
165
166      jpg_rgbs = []
167      for i, size in enumerate(
168          capture_request_utils.get_available_output_sizes(
169              _JPG_STR, props, match_ar_size=match_ar)):
170        jpg_rgbs.append(do_capture_and_extract_rgb_means(
171            req, cam, props, size, _JPG_STR, i, name_with_log_path, debug))
172
173      # Plot means vs format
174      pylab.figure(_NAME)
175      pylab.title(_NAME)
176      yuv_index = range(len(yuv_rgbs))
177      jpg_index = range(len(jpg_rgbs))
178      pylab.plot(yuv_index, [rgb[0] for rgb in yuv_rgbs],
179                 '-ro', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE)
180      pylab.plot(yuv_index, [rgb[1] for rgb in yuv_rgbs],
181                 '-go', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE)
182      pylab.plot(yuv_index, [rgb[2] for rgb in yuv_rgbs],
183                 '-bo', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE)
184      pylab.plot(jpg_index, [rgb[0] for rgb in jpg_rgbs],
185                 '-r^', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE)
186      pylab.plot(jpg_index, [rgb[1] for rgb in jpg_rgbs],
187                 '-g^', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE)
188      pylab.plot(jpg_index, [rgb[2] for rgb in jpg_rgbs],
189                 '-b^', alpha=_PLOT_ALPHA, markersize=_PLOT_MARKER_SIZE)
190      pylab.ylim([0, 1])
191      ax = pylab.gca()
192      # force matplotlib to use integers for x-axis labels
193      ax.xaxis.set_major_locator(MaxNLocator(integer=True))
194      yuv_marker = mlines.Line2D([], [], linestyle='None',
195                                 color='black', marker='.',
196                                 markersize=_PLOT_LEGEND_CIRCLE_SIZE,
197                                 label='YUV')
198      jpg_marker = mlines.Line2D([], [], linestyle='None',
199                                 color='black', marker='^',
200                                 markersize=_PLOT_LEGEND_TRIANGLE_SIZE,
201                                 label='JPEG')
202      ax.legend(handles=[yuv_marker, jpg_marker])
203      pylab.xlabel('format number')
204      pylab.ylabel('RGB avg [0, 1]')
205      matplotlib.pyplot.savefig(f'{name_with_log_path}_plot_means.png')
206
207      # Assert all captures are similar in RGB space using rgbs[0] as ref.
208      rgbs = yuv_rgbs + jpg_rgbs
209      max_diff = 0
210      for rgb_i in rgbs[1:]:
211        rms_diff = image_processing_utils.compute_image_rms_difference_1d(
212            rgbs[0], rgb_i)  # use first capture as reference
213        max_diff = max(max_diff, rms_diff)
214      msg = f'Max RMS difference: {max_diff:.4f}'
215      logging.debug('%s', msg)
216      if max_diff >= _THRESHOLD_MAX_RMS_DIFF:
217        raise AssertionError(f'{msg} spec: {_THRESHOLD_MAX_RMS_DIFF}')
218
219if __name__ == '__main__':
220  test_runner.main()
221