xref: /aosp_15_r20/cts/apps/CameraITS/utils/image_processing_utils.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1*b7c941bbSAndroid Build Coastguard Worker# Copyright 2013 The Android Open Source Project
2*b7c941bbSAndroid Build Coastguard Worker#
3*b7c941bbSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*b7c941bbSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*b7c941bbSAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*b7c941bbSAndroid Build Coastguard Worker#
7*b7c941bbSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*b7c941bbSAndroid Build Coastguard Worker#
9*b7c941bbSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*b7c941bbSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*b7c941bbSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*b7c941bbSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*b7c941bbSAndroid Build Coastguard Worker# limitations under the License.
14*b7c941bbSAndroid Build Coastguard Worker"""Image processing utility functions."""
15*b7c941bbSAndroid Build Coastguard Worker
16*b7c941bbSAndroid Build Coastguard Worker
17*b7c941bbSAndroid Build Coastguard Workerimport copy
18*b7c941bbSAndroid Build Coastguard Workerimport io
19*b7c941bbSAndroid Build Coastguard Workerimport logging
20*b7c941bbSAndroid Build Coastguard Workerimport math
21*b7c941bbSAndroid Build Coastguard Workerimport matplotlib
22*b7c941bbSAndroid Build Coastguard Workerfrom matplotlib import pyplot as plt
23*b7c941bbSAndroid Build Coastguard Workerimport os
24*b7c941bbSAndroid Build Coastguard Workerimport sys
25*b7c941bbSAndroid Build Coastguard Worker
26*b7c941bbSAndroid Build Coastguard Workerimport capture_request_utils
27*b7c941bbSAndroid Build Coastguard Workerimport error_util
28*b7c941bbSAndroid Build Coastguard Workerimport noise_model_constants
29*b7c941bbSAndroid Build Coastguard Workerimport numpy
30*b7c941bbSAndroid Build Coastguard Workerfrom PIL import Image
31*b7c941bbSAndroid Build Coastguard Workerfrom PIL import ImageCms
32*b7c941bbSAndroid Build Coastguard Worker
33*b7c941bbSAndroid Build Coastguard Worker
34*b7c941bbSAndroid Build Coastguard Worker_CMAP_BLUE = ('black', 'blue', 'lightblue')
35*b7c941bbSAndroid Build Coastguard Worker_CMAP_GREEN = ('black', 'green', 'lightgreen')
36*b7c941bbSAndroid Build Coastguard Worker_CMAP_RED = ('black', 'red', 'lightcoral')
37*b7c941bbSAndroid Build Coastguard Worker_CMAP_SIZE = 6  # 6 inches
38*b7c941bbSAndroid Build Coastguard Worker_NUM_RAW_CHANNELS = 4  # R, Gr, Gb, B
39*b7c941bbSAndroid Build Coastguard Worker
40*b7c941bbSAndroid Build Coastguard WorkerLENS_SHADING_MAP_ON = 1
41*b7c941bbSAndroid Build Coastguard Worker
42*b7c941bbSAndroid Build Coastguard Worker# The matrix is from JFIF spec
43*b7c941bbSAndroid Build Coastguard WorkerDEFAULT_YUV_TO_RGB_CCM = numpy.matrix([[1.000, 0.000, 1.402],
44*b7c941bbSAndroid Build Coastguard Worker                                       [1.000, -0.344, -0.714],
45*b7c941bbSAndroid Build Coastguard Worker                                       [1.000, 1.772, 0.000]])
46*b7c941bbSAndroid Build Coastguard Worker
47*b7c941bbSAndroid Build Coastguard WorkerDEFAULT_YUV_OFFSETS = numpy.array([0, 128, 128], dtype=numpy.uint8)
48*b7c941bbSAndroid Build Coastguard WorkerMAX_LUT_SIZE = 65536
49*b7c941bbSAndroid Build Coastguard WorkerDEFAULT_GAMMA_LUT = numpy.array([
50*b7c941bbSAndroid Build Coastguard Worker    math.floor((MAX_LUT_SIZE-1) * math.pow(i/(MAX_LUT_SIZE-1), 1/2.2) + 0.5)
51*b7c941bbSAndroid Build Coastguard Worker    for i in range(MAX_LUT_SIZE)])
52*b7c941bbSAndroid Build Coastguard WorkerRGB2GRAY_WEIGHTS = (0.299, 0.587, 0.114)
53*b7c941bbSAndroid Build Coastguard WorkerTEST_IMG_DIR = os.path.join(os.environ['CAMERA_ITS_TOP'], 'test_images')
54*b7c941bbSAndroid Build Coastguard Worker
55*b7c941bbSAndroid Build Coastguard Worker# Expected adapted primaries in ICC profile per color space
56*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_RX_P3 = 0.682
57*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_RY_P3 = 0.319
58*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_GX_P3 = 0.285
59*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_GY_P3 = 0.675
60*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_BX_P3 = 0.156
61*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_BY_P3 = 0.066
62*b7c941bbSAndroid Build Coastguard Worker
63*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_RX_SRGB = 0.648
64*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_RY_SRGB = 0.331
65*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_GX_SRGB = 0.321
66*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_GY_SRGB = 0.598
67*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_BX_SRGB = 0.156
68*b7c941bbSAndroid Build Coastguard WorkerEXPECTED_BY_SRGB = 0.066
69*b7c941bbSAndroid Build Coastguard Worker
70*b7c941bbSAndroid Build Coastguard Worker# Color conversion matrix for DISPLAY P3 to CIEXYZ
71*b7c941bbSAndroid Build Coastguard WorkerP3_TO_XYZ = numpy.array([
72*b7c941bbSAndroid Build Coastguard Worker    [0.5151187, 0.2919778, 0.1571035],
73*b7c941bbSAndroid Build Coastguard Worker    [0.2411892, 0.6922441, 0.0665668],
74*b7c941bbSAndroid Build Coastguard Worker    [-0.0010505, 0.0418791, 0.7840713]
75*b7c941bbSAndroid Build Coastguard Worker]).transpose()
76*b7c941bbSAndroid Build Coastguard Worker
77*b7c941bbSAndroid Build Coastguard Worker# Chosen empirically - tolerance for the point in triangle test for colorspace
78*b7c941bbSAndroid Build Coastguard Worker# chromaticities
79*b7c941bbSAndroid Build Coastguard WorkerCOLORSPACE_TRIANGLE_AREA_TOL = 0.00039
80*b7c941bbSAndroid Build Coastguard Worker
81*b7c941bbSAndroid Build Coastguard Worker
82*b7c941bbSAndroid Build Coastguard Workerdef plot_lsc_maps(lsc_maps, plot_name, test_name_with_log_path):
83*b7c941bbSAndroid Build Coastguard Worker  """Plot the lens shading correction maps.
84*b7c941bbSAndroid Build Coastguard Worker
85*b7c941bbSAndroid Build Coastguard Worker  Args:
86*b7c941bbSAndroid Build Coastguard Worker    lsc_maps: 4D np array; r, gr, gb, b lens shading correction maps.
87*b7c941bbSAndroid Build Coastguard Worker    plot_name: str; identifier for maps ('full_scale' or 'metadata').
88*b7c941bbSAndroid Build Coastguard Worker    test_name_with_log_path: str; test name with log_path location.
89*b7c941bbSAndroid Build Coastguard Worker
90*b7c941bbSAndroid Build Coastguard Worker  Returns:
91*b7c941bbSAndroid Build Coastguard Worker    None, but generates and saves plots.
92*b7c941bbSAndroid Build Coastguard Worker  """
93*b7c941bbSAndroid Build Coastguard Worker  aspect_ratio = lsc_maps[:, :, 0].shape[1] / lsc_maps[:, :, 0].shape[0]
94*b7c941bbSAndroid Build Coastguard Worker  plot_w = 1 + aspect_ratio * _CMAP_SIZE  # add 1 for heatmap legend
95*b7c941bbSAndroid Build Coastguard Worker  plt.figure(plot_name, figsize=(plot_w, _CMAP_SIZE))
96*b7c941bbSAndroid Build Coastguard Worker  plt.suptitle(plot_name)
97*b7c941bbSAndroid Build Coastguard Worker
98*b7c941bbSAndroid Build Coastguard Worker  plt.subplot(2, 2, 1)  # 2x2 top left
99*b7c941bbSAndroid Build Coastguard Worker  plt.title('R')
100*b7c941bbSAndroid Build Coastguard Worker  cmap = matplotlib.colors.LinearSegmentedColormap.from_list('', _CMAP_RED)
101*b7c941bbSAndroid Build Coastguard Worker  plt.pcolormesh(lsc_maps[:, :, 0], cmap=cmap)
102*b7c941bbSAndroid Build Coastguard Worker  plt.colorbar()
103*b7c941bbSAndroid Build Coastguard Worker
104*b7c941bbSAndroid Build Coastguard Worker  plt.subplot(2, 2, 2)  # 2x2 top right
105*b7c941bbSAndroid Build Coastguard Worker  plt.title('Gr')
106*b7c941bbSAndroid Build Coastguard Worker  cmap = matplotlib.colors.LinearSegmentedColormap.from_list('', _CMAP_GREEN)
107*b7c941bbSAndroid Build Coastguard Worker  plt.pcolormesh(lsc_maps[:, :, 1], cmap=cmap)
108*b7c941bbSAndroid Build Coastguard Worker  plt.colorbar()
109*b7c941bbSAndroid Build Coastguard Worker
110*b7c941bbSAndroid Build Coastguard Worker  plt.subplot(2, 2, 3)  # 2x2 bottom left
111*b7c941bbSAndroid Build Coastguard Worker  plt.title('Gb')
112*b7c941bbSAndroid Build Coastguard Worker  cmap = matplotlib.colors.LinearSegmentedColormap.from_list('', _CMAP_GREEN)
113*b7c941bbSAndroid Build Coastguard Worker  plt.pcolormesh(lsc_maps[:, :, 2], cmap=cmap)
114*b7c941bbSAndroid Build Coastguard Worker  plt.colorbar()
115*b7c941bbSAndroid Build Coastguard Worker
116*b7c941bbSAndroid Build Coastguard Worker  plt.subplot(2, 2, 4)  # 2x2 bottom right
117*b7c941bbSAndroid Build Coastguard Worker  plt.title('B')
118*b7c941bbSAndroid Build Coastguard Worker  cmap = matplotlib.colors.LinearSegmentedColormap.from_list('', _CMAP_BLUE)
119*b7c941bbSAndroid Build Coastguard Worker  plt.pcolormesh(lsc_maps[:, :, 3], cmap=cmap)
120*b7c941bbSAndroid Build Coastguard Worker  plt.colorbar()
121*b7c941bbSAndroid Build Coastguard Worker
122*b7c941bbSAndroid Build Coastguard Worker  plt.savefig(f'{test_name_with_log_path}_{plot_name}_cmaps.png')
123*b7c941bbSAndroid Build Coastguard Worker
124*b7c941bbSAndroid Build Coastguard Worker
125*b7c941bbSAndroid Build Coastguard Workerdef capture_scene_image(cam, props, name_with_log_path):
126*b7c941bbSAndroid Build Coastguard Worker  """Take a picture of the scene on test FAIL."""
127*b7c941bbSAndroid Build Coastguard Worker  req = capture_request_utils.auto_capture_request()
128*b7c941bbSAndroid Build Coastguard Worker  img = convert_capture_to_rgb_image(
129*b7c941bbSAndroid Build Coastguard Worker      cam.do_capture(req, cam.CAP_YUV), props=props)
130*b7c941bbSAndroid Build Coastguard Worker  write_image(img, f'{name_with_log_path}_scene.jpg', True)
131*b7c941bbSAndroid Build Coastguard Worker
132*b7c941bbSAndroid Build Coastguard Worker
133*b7c941bbSAndroid Build Coastguard Workerdef convert_image_to_uint8(image):
134*b7c941bbSAndroid Build Coastguard Worker  image = image*255
135*b7c941bbSAndroid Build Coastguard Worker  return image.astype(numpy.uint8)
136*b7c941bbSAndroid Build Coastguard Worker
137*b7c941bbSAndroid Build Coastguard Worker
138*b7c941bbSAndroid Build Coastguard Workerdef assert_props_is_not_none(props):
139*b7c941bbSAndroid Build Coastguard Worker  if not props:
140*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('props is None')
141*b7c941bbSAndroid Build Coastguard Worker
142*b7c941bbSAndroid Build Coastguard Worker
143*b7c941bbSAndroid Build Coastguard Workerdef assert_capture_width_and_height(cap, width, height):
144*b7c941bbSAndroid Build Coastguard Worker  if cap['width'] != width or cap['height'] != height:
145*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
146*b7c941bbSAndroid Build Coastguard Worker        'Unexpected capture WxH size, expected [{}x{}], actual [{}x{}]'.format(
147*b7c941bbSAndroid Build Coastguard Worker            width, height, cap['width'], cap['height']
148*b7c941bbSAndroid Build Coastguard Worker        )
149*b7c941bbSAndroid Build Coastguard Worker    )
150*b7c941bbSAndroid Build Coastguard Worker
151*b7c941bbSAndroid Build Coastguard Worker
152*b7c941bbSAndroid Build Coastguard Workerdef apply_lens_shading_map(color_plane, black_level, white_level, lsc_map):
153*b7c941bbSAndroid Build Coastguard Worker  """Apply the lens shading map to the color plane.
154*b7c941bbSAndroid Build Coastguard Worker
155*b7c941bbSAndroid Build Coastguard Worker  Args:
156*b7c941bbSAndroid Build Coastguard Worker    color_plane: 2D np array for color plane with values [0.0, 1.0].
157*b7c941bbSAndroid Build Coastguard Worker    black_level: float; black level for the color plane.
158*b7c941bbSAndroid Build Coastguard Worker    white_level: int; full scale for the color plane.
159*b7c941bbSAndroid Build Coastguard Worker    lsc_map: 2D np array lens shading matching size of color_plane.
160*b7c941bbSAndroid Build Coastguard Worker
161*b7c941bbSAndroid Build Coastguard Worker  Returns:
162*b7c941bbSAndroid Build Coastguard Worker    color_plane with lsc applied.
163*b7c941bbSAndroid Build Coastguard Worker  """
164*b7c941bbSAndroid Build Coastguard Worker  logging.debug('color plane pre-lsc min, max: %.4f, %.4f',
165*b7c941bbSAndroid Build Coastguard Worker                numpy.min(color_plane), numpy.max(color_plane))
166*b7c941bbSAndroid Build Coastguard Worker  color_plane = (numpy.multiply((color_plane * white_level - black_level),
167*b7c941bbSAndroid Build Coastguard Worker                                lsc_map)
168*b7c941bbSAndroid Build Coastguard Worker                 + black_level) / white_level
169*b7c941bbSAndroid Build Coastguard Worker  logging.debug('color plane post-lsc min, max: %.4f, %.4f',
170*b7c941bbSAndroid Build Coastguard Worker                numpy.min(color_plane), numpy.max(color_plane))
171*b7c941bbSAndroid Build Coastguard Worker  return color_plane
172*b7c941bbSAndroid Build Coastguard Worker
173*b7c941bbSAndroid Build Coastguard Worker
174*b7c941bbSAndroid Build Coastguard Workerdef populate_lens_shading_map(img_shape, lsc_map):
175*b7c941bbSAndroid Build Coastguard Worker  """Helper function to create LSC coeifficients for RAW image.
176*b7c941bbSAndroid Build Coastguard Worker
177*b7c941bbSAndroid Build Coastguard Worker  Args:
178*b7c941bbSAndroid Build Coastguard Worker    img_shape: tuple; RAW image shape.
179*b7c941bbSAndroid Build Coastguard Worker    lsc_map: 2D low resolution array with lens shading map values.
180*b7c941bbSAndroid Build Coastguard Worker
181*b7c941bbSAndroid Build Coastguard Worker  Returns:
182*b7c941bbSAndroid Build Coastguard Worker    value for lens shading map at point (x, y) in the image.
183*b7c941bbSAndroid Build Coastguard Worker  """
184*b7c941bbSAndroid Build Coastguard Worker  img_w, img_h = img_shape[1], img_shape[0]
185*b7c941bbSAndroid Build Coastguard Worker  map_w, map_h = lsc_map.shape[1], lsc_map.shape[0]
186*b7c941bbSAndroid Build Coastguard Worker
187*b7c941bbSAndroid Build Coastguard Worker  x, y = numpy.meshgrid(numpy.arange(img_w), numpy.arange(img_h))
188*b7c941bbSAndroid Build Coastguard Worker
189*b7c941bbSAndroid Build Coastguard Worker  # (u,v) is lsc map location, values [0, map_w-1], [0, map_h-1]
190*b7c941bbSAndroid Build Coastguard Worker  # Vectorized calculations
191*b7c941bbSAndroid Build Coastguard Worker  u = x * (map_w - 1) / (img_w - 1)
192*b7c941bbSAndroid Build Coastguard Worker  v = y * (map_h - 1) / (img_h - 1)
193*b7c941bbSAndroid Build Coastguard Worker  u_min = numpy.floor(u).astype(int)
194*b7c941bbSAndroid Build Coastguard Worker  v_min = numpy.floor(v).astype(int)
195*b7c941bbSAndroid Build Coastguard Worker  u_frac = u - u_min
196*b7c941bbSAndroid Build Coastguard Worker  v_frac = v - v_min
197*b7c941bbSAndroid Build Coastguard Worker  u_max = numpy.where(u_frac > 0, u_min + 1, u_min)
198*b7c941bbSAndroid Build Coastguard Worker  v_max = numpy.where(v_frac > 0, v_min + 1, v_min)
199*b7c941bbSAndroid Build Coastguard Worker
200*b7c941bbSAndroid Build Coastguard Worker  # Gather LSC values, handling edge cases (optional)
201*b7c941bbSAndroid Build Coastguard Worker  lsc_tl = lsc_map[(v_min, u_min)]
202*b7c941bbSAndroid Build Coastguard Worker  lsc_tr = lsc_map[(v_min, u_max)]
203*b7c941bbSAndroid Build Coastguard Worker  lsc_bl = lsc_map[(v_max, u_min)]
204*b7c941bbSAndroid Build Coastguard Worker  lsc_br = lsc_map[(v_max, u_max)]
205*b7c941bbSAndroid Build Coastguard Worker
206*b7c941bbSAndroid Build Coastguard Worker  # Bilinear interpolation (vectorized)
207*b7c941bbSAndroid Build Coastguard Worker  lsc_t = lsc_tl * (1 - u_frac) + lsc_tr * u_frac
208*b7c941bbSAndroid Build Coastguard Worker  lsc_b = lsc_bl * (1 - u_frac) + lsc_br * u_frac
209*b7c941bbSAndroid Build Coastguard Worker
210*b7c941bbSAndroid Build Coastguard Worker  return lsc_t * (1 - v_frac) + lsc_b * v_frac
211*b7c941bbSAndroid Build Coastguard Worker
212*b7c941bbSAndroid Build Coastguard Worker
213*b7c941bbSAndroid Build Coastguard Workerdef unpack_lsc_map_from_metadata(metadata):
214*b7c941bbSAndroid Build Coastguard Worker  """Get lens shading correction map from metadata and turn into 3D array.
215*b7c941bbSAndroid Build Coastguard Worker
216*b7c941bbSAndroid Build Coastguard Worker  Args:
217*b7c941bbSAndroid Build Coastguard Worker    metadata: dict; metadata from RAW capture.
218*b7c941bbSAndroid Build Coastguard Worker
219*b7c941bbSAndroid Build Coastguard Worker  Returns:
220*b7c941bbSAndroid Build Coastguard Worker    3D numpy array of lens shading maps.
221*b7c941bbSAndroid Build Coastguard Worker  """
222*b7c941bbSAndroid Build Coastguard Worker  lsc_metadata = metadata['android.statistics.lensShadingCorrectionMap']
223*b7c941bbSAndroid Build Coastguard Worker  lsc_map_w, lsc_map_h = lsc_metadata['width'], lsc_metadata['height']
224*b7c941bbSAndroid Build Coastguard Worker  lsc_map = lsc_metadata['map']
225*b7c941bbSAndroid Build Coastguard Worker  logging.debug(
226*b7c941bbSAndroid Build Coastguard Worker      'lensShadingCorrectionMap (H, W): (%d, %d)', lsc_map_h, lsc_map_w
227*b7c941bbSAndroid Build Coastguard Worker  )
228*b7c941bbSAndroid Build Coastguard Worker  return numpy.array(lsc_map).reshape(lsc_map_h, lsc_map_w, _NUM_RAW_CHANNELS)
229*b7c941bbSAndroid Build Coastguard Worker
230*b7c941bbSAndroid Build Coastguard Worker
231*b7c941bbSAndroid Build Coastguard Workerdef convert_raw_capture_to_rgb_image(cap_raw, props, raw_fmt,
232*b7c941bbSAndroid Build Coastguard Worker                                     log_path_with_name):
233*b7c941bbSAndroid Build Coastguard Worker  """Convert a RAW captured image object to a RGB image.
234*b7c941bbSAndroid Build Coastguard Worker
235*b7c941bbSAndroid Build Coastguard Worker  Args:
236*b7c941bbSAndroid Build Coastguard Worker    cap_raw: A RAW capture object as returned by its_session_utils.do_capture.
237*b7c941bbSAndroid Build Coastguard Worker    props: camera properties object (of static values).
238*b7c941bbSAndroid Build Coastguard Worker    raw_fmt: string of type 'raw', 'raw10', 'raw12'.
239*b7c941bbSAndroid Build Coastguard Worker    log_path_with_name: string with test name and save location.
240*b7c941bbSAndroid Build Coastguard Worker
241*b7c941bbSAndroid Build Coastguard Worker  Returns:
242*b7c941bbSAndroid Build Coastguard Worker    RGB float-3 image array, with pixel values in [0.0, 1.0].
243*b7c941bbSAndroid Build Coastguard Worker  """
244*b7c941bbSAndroid Build Coastguard Worker  shading_mode = cap_raw['metadata']['android.shading.mode']
245*b7c941bbSAndroid Build Coastguard Worker  lens_shading_map_mode = cap_raw[
246*b7c941bbSAndroid Build Coastguard Worker      'metadata'].get('android.statistics.lensShadingMapMode')
247*b7c941bbSAndroid Build Coastguard Worker  lens_shading_applied = props['android.sensor.info.lensShadingApplied']
248*b7c941bbSAndroid Build Coastguard Worker  control_af_mode = cap_raw['metadata']['android.control.afMode']
249*b7c941bbSAndroid Build Coastguard Worker  focus_distance = cap_raw['metadata']['android.lens.focusDistance']
250*b7c941bbSAndroid Build Coastguard Worker  logging.debug('%s capture AF mode: %s', raw_fmt, control_af_mode)
251*b7c941bbSAndroid Build Coastguard Worker  logging.debug('%s capture focus distance: %s', raw_fmt, focus_distance)
252*b7c941bbSAndroid Build Coastguard Worker  logging.debug('%s capture shading mode: %d', raw_fmt, shading_mode)
253*b7c941bbSAndroid Build Coastguard Worker  logging.debug('lensShadingMapApplied: %r', lens_shading_applied)
254*b7c941bbSAndroid Build Coastguard Worker  logging.debug('lensShadingMapMode: %s', lens_shading_map_mode)
255*b7c941bbSAndroid Build Coastguard Worker
256*b7c941bbSAndroid Build Coastguard Worker  # Split RAW to RGB conversion in 2 to allow LSC application (if needed).
257*b7c941bbSAndroid Build Coastguard Worker  r, gr, gb, b = convert_capture_to_planes(cap_raw, props=props)
258*b7c941bbSAndroid Build Coastguard Worker
259*b7c941bbSAndroid Build Coastguard Worker  # get from metadata, upsample, and apply
260*b7c941bbSAndroid Build Coastguard Worker  if lens_shading_map_mode == LENS_SHADING_MAP_ON:
261*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Applying lens shading map')
262*b7c941bbSAndroid Build Coastguard Worker    plot_name_stem_with_log_path = f'{log_path_with_name}_{raw_fmt}'
263*b7c941bbSAndroid Build Coastguard Worker    black_levels = get_black_levels(props, cap_raw)
264*b7c941bbSAndroid Build Coastguard Worker    white_level = int(props['android.sensor.info.whiteLevel'])
265*b7c941bbSAndroid Build Coastguard Worker    lsc_maps = unpack_lsc_map_from_metadata(cap_raw['metadata'])
266*b7c941bbSAndroid Build Coastguard Worker    plot_lsc_maps(lsc_maps, 'metadata', plot_name_stem_with_log_path)
267*b7c941bbSAndroid Build Coastguard Worker    lsc_map_fs_r = populate_lens_shading_map(r.shape, lsc_maps[:, :, 0])
268*b7c941bbSAndroid Build Coastguard Worker    lsc_map_fs_gr = populate_lens_shading_map(gr.shape, lsc_maps[:, :, 1])
269*b7c941bbSAndroid Build Coastguard Worker    lsc_map_fs_gb = populate_lens_shading_map(gb.shape, lsc_maps[:, :, 2])
270*b7c941bbSAndroid Build Coastguard Worker    lsc_map_fs_b = populate_lens_shading_map(b.shape, lsc_maps[:, :, 3])
271*b7c941bbSAndroid Build Coastguard Worker    plot_lsc_maps(
272*b7c941bbSAndroid Build Coastguard Worker        numpy.dstack((lsc_map_fs_r, lsc_map_fs_gr, lsc_map_fs_gb,
273*b7c941bbSAndroid Build Coastguard Worker                      lsc_map_fs_b)),
274*b7c941bbSAndroid Build Coastguard Worker        'fullscale', plot_name_stem_with_log_path
275*b7c941bbSAndroid Build Coastguard Worker    )
276*b7c941bbSAndroid Build Coastguard Worker    r = apply_lens_shading_map(
277*b7c941bbSAndroid Build Coastguard Worker        r[:, :, 0], black_levels[0], white_level, lsc_map_fs_r
278*b7c941bbSAndroid Build Coastguard Worker    )
279*b7c941bbSAndroid Build Coastguard Worker    gr = apply_lens_shading_map(
280*b7c941bbSAndroid Build Coastguard Worker        gr[:, :, 0], black_levels[1], white_level, lsc_map_fs_gr
281*b7c941bbSAndroid Build Coastguard Worker    )
282*b7c941bbSAndroid Build Coastguard Worker    gb = apply_lens_shading_map(
283*b7c941bbSAndroid Build Coastguard Worker        gb[:, :, 0], black_levels[2], white_level, lsc_map_fs_gb
284*b7c941bbSAndroid Build Coastguard Worker    )
285*b7c941bbSAndroid Build Coastguard Worker    b = apply_lens_shading_map(
286*b7c941bbSAndroid Build Coastguard Worker        b[:, :, 0], black_levels[3], white_level, lsc_map_fs_b
287*b7c941bbSAndroid Build Coastguard Worker    )
288*b7c941bbSAndroid Build Coastguard Worker  img = convert_raw_to_rgb_image(r, gr, gb, b, props, cap_raw['metadata'])
289*b7c941bbSAndroid Build Coastguard Worker  return img
290*b7c941bbSAndroid Build Coastguard Worker
291*b7c941bbSAndroid Build Coastguard Worker
292*b7c941bbSAndroid Build Coastguard Workerdef convert_capture_to_rgb_image(cap,
293*b7c941bbSAndroid Build Coastguard Worker                                 props=None,
294*b7c941bbSAndroid Build Coastguard Worker                                 apply_ccm_raw_to_rgb=True):
295*b7c941bbSAndroid Build Coastguard Worker  """Convert a captured image object to a RGB image.
296*b7c941bbSAndroid Build Coastguard Worker
297*b7c941bbSAndroid Build Coastguard Worker  Args:
298*b7c941bbSAndroid Build Coastguard Worker     cap: A capture object as returned by its_session_utils.do_capture.
299*b7c941bbSAndroid Build Coastguard Worker     props: (Optional) camera properties object (of static values);
300*b7c941bbSAndroid Build Coastguard Worker            required for processing raw images.
301*b7c941bbSAndroid Build Coastguard Worker     apply_ccm_raw_to_rgb: (Optional) boolean to apply color correction matrix.
302*b7c941bbSAndroid Build Coastguard Worker
303*b7c941bbSAndroid Build Coastguard Worker  Returns:
304*b7c941bbSAndroid Build Coastguard Worker        RGB float-3 image array, with pixel values in [0.0, 1.0].
305*b7c941bbSAndroid Build Coastguard Worker  """
306*b7c941bbSAndroid Build Coastguard Worker  w = cap['width']
307*b7c941bbSAndroid Build Coastguard Worker  h = cap['height']
308*b7c941bbSAndroid Build Coastguard Worker  if cap['format'] == 'raw10' or cap['format'] == 'raw10QuadBayer':
309*b7c941bbSAndroid Build Coastguard Worker    assert_props_is_not_none(props)
310*b7c941bbSAndroid Build Coastguard Worker    is_quad_bayer = cap['format'] == 'raw10QuadBayer'
311*b7c941bbSAndroid Build Coastguard Worker    cap = unpack_raw10_capture(cap, is_quad_bayer)
312*b7c941bbSAndroid Build Coastguard Worker
313*b7c941bbSAndroid Build Coastguard Worker  if cap['format'] == 'raw12':
314*b7c941bbSAndroid Build Coastguard Worker    assert_props_is_not_none(props)
315*b7c941bbSAndroid Build Coastguard Worker    cap = unpack_raw12_capture(cap)
316*b7c941bbSAndroid Build Coastguard Worker
317*b7c941bbSAndroid Build Coastguard Worker  if cap['format'] == 'yuv':
318*b7c941bbSAndroid Build Coastguard Worker    y = cap['data'][0: w * h]
319*b7c941bbSAndroid Build Coastguard Worker    u = cap['data'][w * h: w * h * 5//4]
320*b7c941bbSAndroid Build Coastguard Worker    v = cap['data'][w * h * 5//4: w * h * 6//4]
321*b7c941bbSAndroid Build Coastguard Worker    return convert_yuv420_planar_to_rgb_image(y, u, v, w, h)
322*b7c941bbSAndroid Build Coastguard Worker  elif cap['format'] == 'jpeg' or cap['format'] == 'jpeg_r':
323*b7c941bbSAndroid Build Coastguard Worker    return decompress_jpeg_to_rgb_image(cap['data'])
324*b7c941bbSAndroid Build Coastguard Worker  elif (cap['format'] in ('raw', 'rawQuadBayer') or
325*b7c941bbSAndroid Build Coastguard Worker        cap['format'] in noise_model_constants.VALID_RAW_STATS_FORMATS):
326*b7c941bbSAndroid Build Coastguard Worker    assert_props_is_not_none(props)
327*b7c941bbSAndroid Build Coastguard Worker    r, gr, gb, b = convert_capture_to_planes(cap, props)
328*b7c941bbSAndroid Build Coastguard Worker    return convert_raw_to_rgb_image(
329*b7c941bbSAndroid Build Coastguard Worker        r, gr, gb, b, props, cap['metadata'], apply_ccm_raw_to_rgb)
330*b7c941bbSAndroid Build Coastguard Worker  elif cap['format'] == 'y8':
331*b7c941bbSAndroid Build Coastguard Worker    y = cap['data'][0: w * h]
332*b7c941bbSAndroid Build Coastguard Worker    return convert_y8_to_rgb_image(y, w, h)
333*b7c941bbSAndroid Build Coastguard Worker  else:
334*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError(f"Invalid format {cap['format']}")
335*b7c941bbSAndroid Build Coastguard Worker
336*b7c941bbSAndroid Build Coastguard Worker
337*b7c941bbSAndroid Build Coastguard Workerdef unpack_raw10_capture(cap, is_quad_bayer=False):
338*b7c941bbSAndroid Build Coastguard Worker  """Unpack a raw-10 capture to a raw-16 capture.
339*b7c941bbSAndroid Build Coastguard Worker
340*b7c941bbSAndroid Build Coastguard Worker  Args:
341*b7c941bbSAndroid Build Coastguard Worker    cap: A raw-10 capture object.
342*b7c941bbSAndroid Build Coastguard Worker    is_quad_bayer: Boolean flag for Bayer or Quad Bayer capture.
343*b7c941bbSAndroid Build Coastguard Worker
344*b7c941bbSAndroid Build Coastguard Worker  Returns:
345*b7c941bbSAndroid Build Coastguard Worker    New capture object with raw-16 data.
346*b7c941bbSAndroid Build Coastguard Worker  """
347*b7c941bbSAndroid Build Coastguard Worker  # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
348*b7c941bbSAndroid Build Coastguard Worker  # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
349*b7c941bbSAndroid Build Coastguard Worker  w, h = cap['width'], cap['height']
350*b7c941bbSAndroid Build Coastguard Worker  if w % 4 != 0:
351*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError('Invalid raw-10 buffer width')
352*b7c941bbSAndroid Build Coastguard Worker  cap = copy.deepcopy(cap)
353*b7c941bbSAndroid Build Coastguard Worker  cap['data'] = unpack_raw10_image(cap['data'].reshape(h, w * 5 // 4))
354*b7c941bbSAndroid Build Coastguard Worker  cap['format'] = 'rawQuadBayer' if is_quad_bayer else 'raw'
355*b7c941bbSAndroid Build Coastguard Worker  return cap
356*b7c941bbSAndroid Build Coastguard Worker
357*b7c941bbSAndroid Build Coastguard Worker
358*b7c941bbSAndroid Build Coastguard Workerdef unpack_raw10_image(img):
359*b7c941bbSAndroid Build Coastguard Worker  """Unpack a raw-10 image to a raw-16 image.
360*b7c941bbSAndroid Build Coastguard Worker
361*b7c941bbSAndroid Build Coastguard Worker  Output image will have the 10 LSBs filled in each 16b word, and the 6 MSBs
362*b7c941bbSAndroid Build Coastguard Worker  will be set to zero.
363*b7c941bbSAndroid Build Coastguard Worker
364*b7c941bbSAndroid Build Coastguard Worker  Args:
365*b7c941bbSAndroid Build Coastguard Worker    img: A raw-10 image, as a uint8 numpy array.
366*b7c941bbSAndroid Build Coastguard Worker
367*b7c941bbSAndroid Build Coastguard Worker  Returns:
368*b7c941bbSAndroid Build Coastguard Worker    Image as a uint16 numpy array, with all row padding stripped.
369*b7c941bbSAndroid Build Coastguard Worker  """
370*b7c941bbSAndroid Build Coastguard Worker  if img.shape[1] % 5 != 0:
371*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError('Invalid raw-10 buffer width')
372*b7c941bbSAndroid Build Coastguard Worker  w = img.shape[1] * 4 // 5
373*b7c941bbSAndroid Build Coastguard Worker  h = img.shape[0]
374*b7c941bbSAndroid Build Coastguard Worker  # Cut out the 4x8b MSBs and shift to bits [9:2] in 16b words.
375*b7c941bbSAndroid Build Coastguard Worker  msbs = numpy.delete(img, numpy.s_[4::5], 1)
376*b7c941bbSAndroid Build Coastguard Worker  msbs = msbs.astype(numpy.uint16)
377*b7c941bbSAndroid Build Coastguard Worker  msbs = numpy.left_shift(msbs, 2)
378*b7c941bbSAndroid Build Coastguard Worker  msbs = msbs.reshape(h, w)
379*b7c941bbSAndroid Build Coastguard Worker  # Cut out the 4x2b LSBs and put each in bits [1:0] of their own 8b words.
380*b7c941bbSAndroid Build Coastguard Worker  lsbs = img[::, 4::5].reshape(h, w // 4)
381*b7c941bbSAndroid Build Coastguard Worker  lsbs = numpy.right_shift(
382*b7c941bbSAndroid Build Coastguard Worker      numpy.packbits(numpy.unpackbits(lsbs).reshape((h, w // 4, 4, 2)), 3), 6)
383*b7c941bbSAndroid Build Coastguard Worker  # Pair the LSB bits group to 0th pixel instead of 3rd pixel
384*b7c941bbSAndroid Build Coastguard Worker  lsbs = lsbs.reshape(h, w // 4, 4)[:, :, ::-1]
385*b7c941bbSAndroid Build Coastguard Worker  lsbs = lsbs.reshape(h, w)
386*b7c941bbSAndroid Build Coastguard Worker  # Fuse the MSBs and LSBs back together
387*b7c941bbSAndroid Build Coastguard Worker  img16 = numpy.bitwise_or(msbs, lsbs).reshape(h, w)
388*b7c941bbSAndroid Build Coastguard Worker  return img16
389*b7c941bbSAndroid Build Coastguard Worker
390*b7c941bbSAndroid Build Coastguard Worker
391*b7c941bbSAndroid Build Coastguard Workerdef unpack_raw12_capture(cap):
392*b7c941bbSAndroid Build Coastguard Worker  """Unpack a raw-12 capture to a raw-16 capture.
393*b7c941bbSAndroid Build Coastguard Worker
394*b7c941bbSAndroid Build Coastguard Worker  Args:
395*b7c941bbSAndroid Build Coastguard Worker    cap: A raw-12 capture object.
396*b7c941bbSAndroid Build Coastguard Worker
397*b7c941bbSAndroid Build Coastguard Worker  Returns:
398*b7c941bbSAndroid Build Coastguard Worker     New capture object with raw-16 data.
399*b7c941bbSAndroid Build Coastguard Worker  """
400*b7c941bbSAndroid Build Coastguard Worker  # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
401*b7c941bbSAndroid Build Coastguard Worker  # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
402*b7c941bbSAndroid Build Coastguard Worker  w, h = cap['width'], cap['height']
403*b7c941bbSAndroid Build Coastguard Worker  if w % 2 != 0:
404*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError('Invalid raw-12 buffer width')
405*b7c941bbSAndroid Build Coastguard Worker  cap = copy.deepcopy(cap)
406*b7c941bbSAndroid Build Coastguard Worker  cap['data'] = unpack_raw12_image(cap['data'].reshape(h, w * 3 // 2))
407*b7c941bbSAndroid Build Coastguard Worker  cap['format'] = 'raw'
408*b7c941bbSAndroid Build Coastguard Worker  return cap
409*b7c941bbSAndroid Build Coastguard Worker
410*b7c941bbSAndroid Build Coastguard Worker
411*b7c941bbSAndroid Build Coastguard Workerdef unpack_raw12_image(img):
412*b7c941bbSAndroid Build Coastguard Worker  """Unpack a raw-12 image to a raw-16 image.
413*b7c941bbSAndroid Build Coastguard Worker
414*b7c941bbSAndroid Build Coastguard Worker  Output image will have the 12 LSBs filled in each 16b word, and the 4 MSBs
415*b7c941bbSAndroid Build Coastguard Worker  will be set to zero.
416*b7c941bbSAndroid Build Coastguard Worker
417*b7c941bbSAndroid Build Coastguard Worker  Args:
418*b7c941bbSAndroid Build Coastguard Worker   img: A raw-12 image, as a uint8 numpy array.
419*b7c941bbSAndroid Build Coastguard Worker
420*b7c941bbSAndroid Build Coastguard Worker  Returns:
421*b7c941bbSAndroid Build Coastguard Worker    Image as a uint16 numpy array, with all row padding stripped.
422*b7c941bbSAndroid Build Coastguard Worker  """
423*b7c941bbSAndroid Build Coastguard Worker  if img.shape[1] % 3 != 0:
424*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError('Invalid raw-12 buffer width')
425*b7c941bbSAndroid Build Coastguard Worker  w = img.shape[1] * 2 // 3
426*b7c941bbSAndroid Build Coastguard Worker  h = img.shape[0]
427*b7c941bbSAndroid Build Coastguard Worker  # Cut out the 2x8b MSBs and shift to bits [11:4] in 16b words.
428*b7c941bbSAndroid Build Coastguard Worker  msbs = numpy.delete(img, numpy.s_[2::3], 1)
429*b7c941bbSAndroid Build Coastguard Worker  msbs = msbs.astype(numpy.uint16)
430*b7c941bbSAndroid Build Coastguard Worker  msbs = numpy.left_shift(msbs, 4)
431*b7c941bbSAndroid Build Coastguard Worker  msbs = msbs.reshape(h, w)
432*b7c941bbSAndroid Build Coastguard Worker  # Cut out the 2x4b LSBs and put each in bits [3:0] of their own 8b words.
433*b7c941bbSAndroid Build Coastguard Worker  lsbs = img[::, 2::3].reshape(h, w // 2)
434*b7c941bbSAndroid Build Coastguard Worker  lsbs = numpy.right_shift(
435*b7c941bbSAndroid Build Coastguard Worker      numpy.packbits(numpy.unpackbits(lsbs).reshape((h, w // 2, 2, 4)), 3), 4)
436*b7c941bbSAndroid Build Coastguard Worker  # Pair the LSB bits group to pixel 0 instead of pixel 1
437*b7c941bbSAndroid Build Coastguard Worker  lsbs = lsbs.reshape(h, w // 2, 2)[:, :, ::-1]
438*b7c941bbSAndroid Build Coastguard Worker  lsbs = lsbs.reshape(h, w)
439*b7c941bbSAndroid Build Coastguard Worker  # Fuse the MSBs and LSBs back together
440*b7c941bbSAndroid Build Coastguard Worker  img16 = numpy.bitwise_or(msbs, lsbs).reshape(h, w)
441*b7c941bbSAndroid Build Coastguard Worker  return img16
442*b7c941bbSAndroid Build Coastguard Worker
443*b7c941bbSAndroid Build Coastguard Worker
444*b7c941bbSAndroid Build Coastguard Workerdef convert_yuv420_planar_to_rgb_image(y_plane, u_plane, v_plane,
445*b7c941bbSAndroid Build Coastguard Worker                                       w, h,
446*b7c941bbSAndroid Build Coastguard Worker                                       ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
447*b7c941bbSAndroid Build Coastguard Worker                                       yuv_off=DEFAULT_YUV_OFFSETS):
448*b7c941bbSAndroid Build Coastguard Worker  """Convert a YUV420 8-bit planar image to an RGB image.
449*b7c941bbSAndroid Build Coastguard Worker
450*b7c941bbSAndroid Build Coastguard Worker  Args:
451*b7c941bbSAndroid Build Coastguard Worker    y_plane: The packed 8-bit Y plane.
452*b7c941bbSAndroid Build Coastguard Worker    u_plane: The packed 8-bit U plane.
453*b7c941bbSAndroid Build Coastguard Worker    v_plane: The packed 8-bit V plane.
454*b7c941bbSAndroid Build Coastguard Worker    w: The width of the image.
455*b7c941bbSAndroid Build Coastguard Worker    h: The height of the image.
456*b7c941bbSAndroid Build Coastguard Worker    ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
457*b7c941bbSAndroid Build Coastguard Worker    yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
458*b7c941bbSAndroid Build Coastguard Worker
459*b7c941bbSAndroid Build Coastguard Worker  Returns:
460*b7c941bbSAndroid Build Coastguard Worker    RGB float-3 image array, with pixel values in [0.0, 1.0].
461*b7c941bbSAndroid Build Coastguard Worker  """
462*b7c941bbSAndroid Build Coastguard Worker  y = numpy.subtract(y_plane, yuv_off[0])
463*b7c941bbSAndroid Build Coastguard Worker  u = numpy.subtract(u_plane, yuv_off[1]).view(numpy.int8)
464*b7c941bbSAndroid Build Coastguard Worker  v = numpy.subtract(v_plane, yuv_off[2]).view(numpy.int8)
465*b7c941bbSAndroid Build Coastguard Worker  u = u.reshape(h // 2, w // 2).repeat(2, axis=1).repeat(2, axis=0)
466*b7c941bbSAndroid Build Coastguard Worker  v = v.reshape(h // 2, w // 2).repeat(2, axis=1).repeat(2, axis=0)
467*b7c941bbSAndroid Build Coastguard Worker  yuv = numpy.dstack([y, u.reshape(w * h), v.reshape(w * h)])
468*b7c941bbSAndroid Build Coastguard Worker  flt = numpy.empty([h, w, 3], dtype=numpy.float32)
469*b7c941bbSAndroid Build Coastguard Worker  flt.reshape(w * h * 3)[:] = yuv.reshape(h * w * 3)[:]
470*b7c941bbSAndroid Build Coastguard Worker  flt = numpy.dot(flt.reshape(w * h, 3), ccm_yuv_to_rgb.T).clip(0, 255)
471*b7c941bbSAndroid Build Coastguard Worker  rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
472*b7c941bbSAndroid Build Coastguard Worker  rgb.reshape(w * h * 3)[:] = flt.reshape(w * h * 3)[:]
473*b7c941bbSAndroid Build Coastguard Worker  return rgb.astype(numpy.float32) / 255.0
474*b7c941bbSAndroid Build Coastguard Worker
475*b7c941bbSAndroid Build Coastguard Worker
476*b7c941bbSAndroid Build Coastguard Workerdef decompress_jpeg_to_rgb_image(jpeg_buffer):
477*b7c941bbSAndroid Build Coastguard Worker  """Decompress a JPEG-compressed image, returning as an RGB image.
478*b7c941bbSAndroid Build Coastguard Worker
479*b7c941bbSAndroid Build Coastguard Worker  Args:
480*b7c941bbSAndroid Build Coastguard Worker    jpeg_buffer: The JPEG stream.
481*b7c941bbSAndroid Build Coastguard Worker
482*b7c941bbSAndroid Build Coastguard Worker  Returns:
483*b7c941bbSAndroid Build Coastguard Worker     A numpy array for the RGB image, with pixels in [0,1].
484*b7c941bbSAndroid Build Coastguard Worker  """
485*b7c941bbSAndroid Build Coastguard Worker  img = Image.open(io.BytesIO(jpeg_buffer))
486*b7c941bbSAndroid Build Coastguard Worker  w = img.size[0]
487*b7c941bbSAndroid Build Coastguard Worker  h = img.size[1]
488*b7c941bbSAndroid Build Coastguard Worker  return numpy.array(img).reshape((h, w, 3)) / 255.0
489*b7c941bbSAndroid Build Coastguard Worker
490*b7c941bbSAndroid Build Coastguard Worker
491*b7c941bbSAndroid Build Coastguard Workerdef decompress_jpeg_to_yuv_image(jpeg_buffer):
492*b7c941bbSAndroid Build Coastguard Worker  """Decompress a JPEG-compressed image, returning as a YUV image.
493*b7c941bbSAndroid Build Coastguard Worker
494*b7c941bbSAndroid Build Coastguard Worker  Args:
495*b7c941bbSAndroid Build Coastguard Worker    jpeg_buffer: The JPEG stream.
496*b7c941bbSAndroid Build Coastguard Worker
497*b7c941bbSAndroid Build Coastguard Worker  Returns:
498*b7c941bbSAndroid Build Coastguard Worker     A numpy array for the YUV image, with pixels in [0,1].
499*b7c941bbSAndroid Build Coastguard Worker  """
500*b7c941bbSAndroid Build Coastguard Worker  img = Image.open(io.BytesIO(jpeg_buffer))
501*b7c941bbSAndroid Build Coastguard Worker  img = img.convert('YCbCr')
502*b7c941bbSAndroid Build Coastguard Worker  w = img.size[0]
503*b7c941bbSAndroid Build Coastguard Worker  h = img.size[1]
504*b7c941bbSAndroid Build Coastguard Worker  return numpy.array(img).reshape((h, w, 3)) / 255.0
505*b7c941bbSAndroid Build Coastguard Worker
506*b7c941bbSAndroid Build Coastguard Worker
507*b7c941bbSAndroid Build Coastguard Workerdef extract_luma_from_patch(cap, patch_x, patch_y, patch_w, patch_h):
508*b7c941bbSAndroid Build Coastguard Worker  """Extract luma from capture."""
509*b7c941bbSAndroid Build Coastguard Worker  y, _, _ = convert_capture_to_planes(cap)
510*b7c941bbSAndroid Build Coastguard Worker  patch = get_image_patch(y, patch_x, patch_y, patch_w, patch_h)
511*b7c941bbSAndroid Build Coastguard Worker  luma = compute_image_means(patch)[0]
512*b7c941bbSAndroid Build Coastguard Worker  return luma
513*b7c941bbSAndroid Build Coastguard Worker
514*b7c941bbSAndroid Build Coastguard Worker
515*b7c941bbSAndroid Build Coastguard Workerdef convert_image_to_numpy_array(image_path):
516*b7c941bbSAndroid Build Coastguard Worker  """Converts image at image_path to numpy array and returns the array.
517*b7c941bbSAndroid Build Coastguard Worker
518*b7c941bbSAndroid Build Coastguard Worker  Args:
519*b7c941bbSAndroid Build Coastguard Worker    image_path: file path
520*b7c941bbSAndroid Build Coastguard Worker
521*b7c941bbSAndroid Build Coastguard Worker  Returns:
522*b7c941bbSAndroid Build Coastguard Worker    numpy array
523*b7c941bbSAndroid Build Coastguard Worker  """
524*b7c941bbSAndroid Build Coastguard Worker  if not os.path.exists(image_path):
525*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f'{image_path} does not exist.')
526*b7c941bbSAndroid Build Coastguard Worker  image = Image.open(image_path)
527*b7c941bbSAndroid Build Coastguard Worker  return numpy.array(image)
528*b7c941bbSAndroid Build Coastguard Worker
529*b7c941bbSAndroid Build Coastguard Worker
530*b7c941bbSAndroid Build Coastguard Workerdef _convert_quad_bayer_img_to_bayer_channels(quad_bayer_img, props=None):
531*b7c941bbSAndroid Build Coastguard Worker  """Convert a quad Bayer image to the Bayer image channels.
532*b7c941bbSAndroid Build Coastguard Worker
533*b7c941bbSAndroid Build Coastguard Worker  Args:
534*b7c941bbSAndroid Build Coastguard Worker      quad_bayer_img: The quad Bayer image.
535*b7c941bbSAndroid Build Coastguard Worker      props: The camera properties.
536*b7c941bbSAndroid Build Coastguard Worker
537*b7c941bbSAndroid Build Coastguard Worker  Returns:
538*b7c941bbSAndroid Build Coastguard Worker      A list of reordered standard Bayer channels of the Bayer image.
539*b7c941bbSAndroid Build Coastguard Worker  """
540*b7c941bbSAndroid Build Coastguard Worker  height, width, num_channels = quad_bayer_img.shape
541*b7c941bbSAndroid Build Coastguard Worker
542*b7c941bbSAndroid Build Coastguard Worker  if num_channels != noise_model_constants.NUM_QUAD_BAYER_CHANNELS:
543*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
544*b7c941bbSAndroid Build Coastguard Worker        'The number of channels in the quad Bayer image must be '
545*b7c941bbSAndroid Build Coastguard Worker        f'{noise_model_constants.NUM_QUAD_BAYER_CHANNELS}.'
546*b7c941bbSAndroid Build Coastguard Worker    )
547*b7c941bbSAndroid Build Coastguard Worker  quad_bayer_cfa_order = get_canonical_cfa_order(props, is_quad_bayer=True)
548*b7c941bbSAndroid Build Coastguard Worker
549*b7c941bbSAndroid Build Coastguard Worker  # Bayer channels are in the order of R, Gr, Gb and B.
550*b7c941bbSAndroid Build Coastguard Worker  bayer_channels = []
551*b7c941bbSAndroid Build Coastguard Worker  for ch in range(4):
552*b7c941bbSAndroid Build Coastguard Worker    channel_img = numpy.zeros(shape=(height, width), dtype='<f')
553*b7c941bbSAndroid Build Coastguard Worker    # Average every four quad Bayer channels into a standard Bayer channel.
554*b7c941bbSAndroid Build Coastguard Worker    for i in quad_bayer_cfa_order[4 * ch: 4 * (ch + 1)]:
555*b7c941bbSAndroid Build Coastguard Worker      channel_img[:, :] += quad_bayer_img[:, :, i]
556*b7c941bbSAndroid Build Coastguard Worker    bayer_channels.append(channel_img / 4)
557*b7c941bbSAndroid Build Coastguard Worker  return bayer_channels
558*b7c941bbSAndroid Build Coastguard Worker
559*b7c941bbSAndroid Build Coastguard Worker
560*b7c941bbSAndroid Build Coastguard Workerdef subsample(image, num_channels=4):
561*b7c941bbSAndroid Build Coastguard Worker  """Subsamples the image to separate its color channels.
562*b7c941bbSAndroid Build Coastguard Worker
563*b7c941bbSAndroid Build Coastguard Worker  Args:
564*b7c941bbSAndroid Build Coastguard Worker    image:        2-D numpy array of raw image.
565*b7c941bbSAndroid Build Coastguard Worker    num_channels: The number of channels in the image.
566*b7c941bbSAndroid Build Coastguard Worker
567*b7c941bbSAndroid Build Coastguard Worker  Returns:
568*b7c941bbSAndroid Build Coastguard Worker    3-D numpy image with each channel separated.
569*b7c941bbSAndroid Build Coastguard Worker  """
570*b7c941bbSAndroid Build Coastguard Worker  if num_channels not in noise_model_constants.VALID_NUM_CHANNELS:
571*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError(
572*b7c941bbSAndroid Build Coastguard Worker        f'Invalid number of channels {num_channels}, which should be in '
573*b7c941bbSAndroid Build Coastguard Worker        f'{noise_model_constants.VALID_NUM_CHANNELS}.'
574*b7c941bbSAndroid Build Coastguard Worker    )
575*b7c941bbSAndroid Build Coastguard Worker
576*b7c941bbSAndroid Build Coastguard Worker  size_h, size_v = image.shape[1], image.shape[0]
577*b7c941bbSAndroid Build Coastguard Worker
578*b7c941bbSAndroid Build Coastguard Worker  # Subsample step size, which is the horizontal or vertical pixel interval
579*b7c941bbSAndroid Build Coastguard Worker  # between two adjacent pixels of the same channel.
580*b7c941bbSAndroid Build Coastguard Worker  stride = int(numpy.sqrt(num_channels))
581*b7c941bbSAndroid Build Coastguard Worker  subsample_img = lambda img, i, h, v, s: img[i // s: v: s, i % s: h: s]
582*b7c941bbSAndroid Build Coastguard Worker  channel_img = numpy.empty((
583*b7c941bbSAndroid Build Coastguard Worker      image.shape[0] // stride,
584*b7c941bbSAndroid Build Coastguard Worker      image.shape[1] // stride,
585*b7c941bbSAndroid Build Coastguard Worker      num_channels,
586*b7c941bbSAndroid Build Coastguard Worker  ))
587*b7c941bbSAndroid Build Coastguard Worker
588*b7c941bbSAndroid Build Coastguard Worker  for i in range(num_channels):
589*b7c941bbSAndroid Build Coastguard Worker    sub_img = subsample_img(image, i, size_h, size_v, stride)
590*b7c941bbSAndroid Build Coastguard Worker    channel_img[:, :, i] = sub_img
591*b7c941bbSAndroid Build Coastguard Worker
592*b7c941bbSAndroid Build Coastguard Worker  return channel_img
593*b7c941bbSAndroid Build Coastguard Worker
594*b7c941bbSAndroid Build Coastguard Worker
595*b7c941bbSAndroid Build Coastguard Workerdef convert_capture_to_planes(cap, props=None):
596*b7c941bbSAndroid Build Coastguard Worker  """Convert a captured image object to separate image planes.
597*b7c941bbSAndroid Build Coastguard Worker
598*b7c941bbSAndroid Build Coastguard Worker  Decompose an image into multiple images, corresponding to different planes.
599*b7c941bbSAndroid Build Coastguard Worker
600*b7c941bbSAndroid Build Coastguard Worker  For YUV420 captures ("yuv"):
601*b7c941bbSAndroid Build Coastguard Worker        Returns Y,U,V planes, where the Y plane is full-res and the U,V planes
602*b7c941bbSAndroid Build Coastguard Worker        are each 1/2 x 1/2 of the full res.
603*b7c941bbSAndroid Build Coastguard Worker
604*b7c941bbSAndroid Build Coastguard Worker    For standard Bayer or quad Bayer captures ("raw", "raw10", "raw12",
605*b7c941bbSAndroid Build Coastguard Worker    "rawQuadBayer", "rawStats", "rawQuadBayerStats", "raw10QuadBayer",
606*b7c941bbSAndroid Build Coastguard Worker    "raw10Stats", "raw10QuadBayerStats"):
607*b7c941bbSAndroid Build Coastguard Worker        Returns planes in the order R, Gr, Gb, B, regardless of the Bayer
608*b7c941bbSAndroid Build Coastguard Worker        pattern layout.
609*b7c941bbSAndroid Build Coastguard Worker        For full-res raw images ("raw", "rawQuadBayer", "raw10",
610*b7c941bbSAndroid Build Coastguard Worker        "raw10QuadBayer", "raw12"), each plane is 1/2 x 1/2 of the full res.
611*b7c941bbSAndroid Build Coastguard Worker        For standard Bayer stats images, the mean image is returned.
612*b7c941bbSAndroid Build Coastguard Worker        For quad Bayer stats images, the average mean image is returned.
613*b7c941bbSAndroid Build Coastguard Worker
614*b7c941bbSAndroid Build Coastguard Worker    For JPEG captures ("jpeg"):
615*b7c941bbSAndroid Build Coastguard Worker        Returns R,G,B full-res planes.
616*b7c941bbSAndroid Build Coastguard Worker
617*b7c941bbSAndroid Build Coastguard Worker  Args:
618*b7c941bbSAndroid Build Coastguard Worker    cap: A capture object as returned by its_session_utils.do_capture.
619*b7c941bbSAndroid Build Coastguard Worker    props: (Optional) camera properties object (of static values);
620*b7c941bbSAndroid Build Coastguard Worker            required for processing raw images.
621*b7c941bbSAndroid Build Coastguard Worker
622*b7c941bbSAndroid Build Coastguard Worker  Returns:
623*b7c941bbSAndroid Build Coastguard Worker    A tuple of float numpy arrays (one per plane), consisting of pixel values
624*b7c941bbSAndroid Build Coastguard Worker    in the range [0.0, 1.0].
625*b7c941bbSAndroid Build Coastguard Worker  """
626*b7c941bbSAndroid Build Coastguard Worker  w = cap['width']
627*b7c941bbSAndroid Build Coastguard Worker  h = cap['height']
628*b7c941bbSAndroid Build Coastguard Worker  if cap['format'] in ('raw10', 'raw10QuadBayer'):
629*b7c941bbSAndroid Build Coastguard Worker    assert_props_is_not_none(props)
630*b7c941bbSAndroid Build Coastguard Worker    is_quad_bayer = cap['format'] == 'raw10QuadBayer'
631*b7c941bbSAndroid Build Coastguard Worker    cap = unpack_raw10_capture(cap, is_quad_bayer)
632*b7c941bbSAndroid Build Coastguard Worker
633*b7c941bbSAndroid Build Coastguard Worker  if cap['format'] == 'raw12':
634*b7c941bbSAndroid Build Coastguard Worker    assert_props_is_not_none(props)
635*b7c941bbSAndroid Build Coastguard Worker    cap = unpack_raw12_capture(cap)
636*b7c941bbSAndroid Build Coastguard Worker  if cap['format'] == 'yuv':
637*b7c941bbSAndroid Build Coastguard Worker    y = cap['data'][0:w * h]
638*b7c941bbSAndroid Build Coastguard Worker    u = cap['data'][w * h:w * h * 5 // 4]
639*b7c941bbSAndroid Build Coastguard Worker    v = cap['data'][w * h * 5 // 4:w * h * 6 // 4]
640*b7c941bbSAndroid Build Coastguard Worker    return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
641*b7c941bbSAndroid Build Coastguard Worker            (u.astype(numpy.float32) / 255.0).reshape(h // 2, w // 2, 1),
642*b7c941bbSAndroid Build Coastguard Worker            (v.astype(numpy.float32) / 255.0).reshape(h // 2, w // 2, 1))
643*b7c941bbSAndroid Build Coastguard Worker  elif cap['format'] == 'jpeg':
644*b7c941bbSAndroid Build Coastguard Worker    rgb = decompress_jpeg_to_rgb_image(cap['data']).reshape(w * h * 3)
645*b7c941bbSAndroid Build Coastguard Worker    return (rgb[::3].reshape(h, w, 1), rgb[1::3].reshape(h, w, 1),
646*b7c941bbSAndroid Build Coastguard Worker            rgb[2::3].reshape(h, w, 1))
647*b7c941bbSAndroid Build Coastguard Worker  elif cap['format'] in ('raw', 'rawQuadBayer'):
648*b7c941bbSAndroid Build Coastguard Worker    assert_props_is_not_none(props)
649*b7c941bbSAndroid Build Coastguard Worker    is_quad_bayer = 'QuadBayer' in cap['format']
650*b7c941bbSAndroid Build Coastguard Worker    white_level = get_white_level(props, cap['metadata'])
651*b7c941bbSAndroid Build Coastguard Worker    img = numpy.ndarray(
652*b7c941bbSAndroid Build Coastguard Worker        shape=(h * w,), dtype='<u2', buffer=cap['data'][0:w * h * 2])
653*b7c941bbSAndroid Build Coastguard Worker    img = img.astype(numpy.float32).reshape(h, w) / white_level
654*b7c941bbSAndroid Build Coastguard Worker    if is_quad_bayer:
655*b7c941bbSAndroid Build Coastguard Worker      pixel_array_size = props.get(
656*b7c941bbSAndroid Build Coastguard Worker          'android.sensor.info.pixelArraySizeMaximumResolution'
657*b7c941bbSAndroid Build Coastguard Worker      )
658*b7c941bbSAndroid Build Coastguard Worker      active_array_size = props.get(
659*b7c941bbSAndroid Build Coastguard Worker          'android.sensor.info.preCorrectionActiveArraySizeMaximumResolution'
660*b7c941bbSAndroid Build Coastguard Worker      )
661*b7c941bbSAndroid Build Coastguard Worker    else:
662*b7c941bbSAndroid Build Coastguard Worker      pixel_array_size = props.get('android.sensor.info.pixelArraySize')
663*b7c941bbSAndroid Build Coastguard Worker      active_array_size = props.get(
664*b7c941bbSAndroid Build Coastguard Worker          'android.sensor.info.preCorrectionActiveArraySize'
665*b7c941bbSAndroid Build Coastguard Worker      )
666*b7c941bbSAndroid Build Coastguard Worker    # Crop the raw image to the active array region.
667*b7c941bbSAndroid Build Coastguard Worker    if pixel_array_size and active_array_size:
668*b7c941bbSAndroid Build Coastguard Worker      # Note that the Rect class is defined such that the left,top values
669*b7c941bbSAndroid Build Coastguard Worker      # are "inside" while the right,bottom values are "outside"; that is,
670*b7c941bbSAndroid Build Coastguard Worker      # it's inclusive of the top,left sides only. So, the width is
671*b7c941bbSAndroid Build Coastguard Worker      # computed as right-left, rather than right-left+1, etc.
672*b7c941bbSAndroid Build Coastguard Worker      wfull = pixel_array_size['width']
673*b7c941bbSAndroid Build Coastguard Worker      hfull = pixel_array_size['height']
674*b7c941bbSAndroid Build Coastguard Worker      xcrop = active_array_size['left']
675*b7c941bbSAndroid Build Coastguard Worker      ycrop = active_array_size['top']
676*b7c941bbSAndroid Build Coastguard Worker      wcrop = active_array_size['right'] - xcrop
677*b7c941bbSAndroid Build Coastguard Worker      hcrop = active_array_size['bottom'] - ycrop
678*b7c941bbSAndroid Build Coastguard Worker      if not wfull >= wcrop >= 0:
679*b7c941bbSAndroid Build Coastguard Worker        raise AssertionError(f'wcrop: {wcrop} not in wfull: {wfull}')
680*b7c941bbSAndroid Build Coastguard Worker      if not hfull >= hcrop >= 0:
681*b7c941bbSAndroid Build Coastguard Worker        raise AssertionError(f'hcrop: {hcrop} not in hfull: {hfull}')
682*b7c941bbSAndroid Build Coastguard Worker      if not wfull - wcrop >= xcrop >= 0:
683*b7c941bbSAndroid Build Coastguard Worker        raise AssertionError(f'xcrop: {xcrop} not in wfull-crop: {wfull-wcrop}')
684*b7c941bbSAndroid Build Coastguard Worker      if not hfull - hcrop >= ycrop >= 0:
685*b7c941bbSAndroid Build Coastguard Worker        raise AssertionError(f'ycrop: {ycrop} not in hfull-crop: {hfull-hcrop}')
686*b7c941bbSAndroid Build Coastguard Worker      if w == wfull and h == hfull:
687*b7c941bbSAndroid Build Coastguard Worker        # Crop needed; extract the center region.
688*b7c941bbSAndroid Build Coastguard Worker        img = img[ycrop:ycrop + hcrop, xcrop:xcrop + wcrop]
689*b7c941bbSAndroid Build Coastguard Worker        w = wcrop
690*b7c941bbSAndroid Build Coastguard Worker        h = hcrop
691*b7c941bbSAndroid Build Coastguard Worker      elif w == wcrop and h == hcrop:
692*b7c941bbSAndroid Build Coastguard Worker        logging.debug('Image is already cropped. No cropping needed.')
693*b7c941bbSAndroid Build Coastguard Worker      else:
694*b7c941bbSAndroid Build Coastguard Worker        raise error_util.CameraItsError('Invalid image size metadata')
695*b7c941bbSAndroid Build Coastguard Worker
696*b7c941bbSAndroid Build Coastguard Worker    idxs = get_canonical_cfa_order(props, is_quad_bayer)
697*b7c941bbSAndroid Build Coastguard Worker    if is_quad_bayer:
698*b7c941bbSAndroid Build Coastguard Worker      # Subsample image array based on the color map.
699*b7c941bbSAndroid Build Coastguard Worker      quad_bayer_img = subsample(
700*b7c941bbSAndroid Build Coastguard Worker          img, noise_model_constants.NUM_QUAD_BAYER_CHANNELS
701*b7c941bbSAndroid Build Coastguard Worker      )
702*b7c941bbSAndroid Build Coastguard Worker      bayer_channels = _convert_quad_bayer_img_to_bayer_channels(
703*b7c941bbSAndroid Build Coastguard Worker          quad_bayer_img, props
704*b7c941bbSAndroid Build Coastguard Worker      )
705*b7c941bbSAndroid Build Coastguard Worker      return bayer_channels
706*b7c941bbSAndroid Build Coastguard Worker    else:
707*b7c941bbSAndroid Build Coastguard Worker      # Separate the image planes.
708*b7c941bbSAndroid Build Coastguard Worker      imgs = [
709*b7c941bbSAndroid Build Coastguard Worker          img[::2].reshape(w * h // 2)[::2].reshape(h // 2, w // 2, 1),
710*b7c941bbSAndroid Build Coastguard Worker          img[::2].reshape(w * h // 2)[1::2].reshape(h // 2, w // 2, 1),
711*b7c941bbSAndroid Build Coastguard Worker          img[1::2].reshape(w * h // 2)[::2].reshape(h // 2, w // 2, 1),
712*b7c941bbSAndroid Build Coastguard Worker          img[1::2].reshape(w * h // 2)[1::2].reshape(h // 2, w // 2, 1),
713*b7c941bbSAndroid Build Coastguard Worker      ]
714*b7c941bbSAndroid Build Coastguard Worker      return [imgs[i] for i in idxs]
715*b7c941bbSAndroid Build Coastguard Worker  elif cap['format'] in (
716*b7c941bbSAndroid Build Coastguard Worker      'rawStats',
717*b7c941bbSAndroid Build Coastguard Worker      'raw10Stats',
718*b7c941bbSAndroid Build Coastguard Worker      'rawQuadBayerStats',
719*b7c941bbSAndroid Build Coastguard Worker      'raw10QuadBayerStats',
720*b7c941bbSAndroid Build Coastguard Worker  ):
721*b7c941bbSAndroid Build Coastguard Worker    assert_props_is_not_none(props)
722*b7c941bbSAndroid Build Coastguard Worker    is_quad_bayer = 'QuadBayer' in cap['format']
723*b7c941bbSAndroid Build Coastguard Worker    white_level = get_white_level(props, cap['metadata'])
724*b7c941bbSAndroid Build Coastguard Worker    if is_quad_bayer:
725*b7c941bbSAndroid Build Coastguard Worker      num_channels = noise_model_constants.NUM_QUAD_BAYER_CHANNELS
726*b7c941bbSAndroid Build Coastguard Worker    else:
727*b7c941bbSAndroid Build Coastguard Worker      num_channels = noise_model_constants.NUM_BAYER_CHANNELS
728*b7c941bbSAndroid Build Coastguard Worker    mean_image, _ = unpack_rawstats_capture(cap, num_channels)
729*b7c941bbSAndroid Build Coastguard Worker    if is_quad_bayer:
730*b7c941bbSAndroid Build Coastguard Worker      bayer_channels = _convert_quad_bayer_img_to_bayer_channels(
731*b7c941bbSAndroid Build Coastguard Worker          mean_image, props
732*b7c941bbSAndroid Build Coastguard Worker      )
733*b7c941bbSAndroid Build Coastguard Worker      bayer_channels = [
734*b7c941bbSAndroid Build Coastguard Worker          bayer_channels[i] / white_level for i in range(len(bayer_channels))
735*b7c941bbSAndroid Build Coastguard Worker      ]
736*b7c941bbSAndroid Build Coastguard Worker      return bayer_channels
737*b7c941bbSAndroid Build Coastguard Worker    else:
738*b7c941bbSAndroid Build Coastguard Worker      # Standard Bayer canonical color channel indices.
739*b7c941bbSAndroid Build Coastguard Worker      idxs = get_canonical_cfa_order(props, is_quad_bayer=False)
740*b7c941bbSAndroid Build Coastguard Worker      # Normalizes the range to [0, 1] without subtracting the black level.
741*b7c941bbSAndroid Build Coastguard Worker      return [mean_image[:, :, i] / white_level for i in idxs]
742*b7c941bbSAndroid Build Coastguard Worker  else:
743*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError(f"Invalid format {cap['format']}")
744*b7c941bbSAndroid Build Coastguard Worker
745*b7c941bbSAndroid Build Coastguard Worker
746*b7c941bbSAndroid Build Coastguard Workerdef downscale_image(img, f):
747*b7c941bbSAndroid Build Coastguard Worker  """Shrink an image by a given integer factor.
748*b7c941bbSAndroid Build Coastguard Worker
749*b7c941bbSAndroid Build Coastguard Worker  This function computes output pixel values by averaging over rectangular
750*b7c941bbSAndroid Build Coastguard Worker  regions of the input image; it doesn't skip or sample pixels, and all input
751*b7c941bbSAndroid Build Coastguard Worker  image pixels are evenly weighted.
752*b7c941bbSAndroid Build Coastguard Worker
753*b7c941bbSAndroid Build Coastguard Worker  If the downscaling factor doesn't cleanly divide the width and/or height,
754*b7c941bbSAndroid Build Coastguard Worker  then the remaining pixels on the right or bottom edge are discarded prior
755*b7c941bbSAndroid Build Coastguard Worker  to the downscaling.
756*b7c941bbSAndroid Build Coastguard Worker
757*b7c941bbSAndroid Build Coastguard Worker  Args:
758*b7c941bbSAndroid Build Coastguard Worker    img: The input image as an ndarray.
759*b7c941bbSAndroid Build Coastguard Worker    f: The downscaling factor, which should be an integer.
760*b7c941bbSAndroid Build Coastguard Worker
761*b7c941bbSAndroid Build Coastguard Worker  Returns:
762*b7c941bbSAndroid Build Coastguard Worker    The new (downscaled) image, as an ndarray.
763*b7c941bbSAndroid Build Coastguard Worker  """
764*b7c941bbSAndroid Build Coastguard Worker  h, w, chans = img.shape
765*b7c941bbSAndroid Build Coastguard Worker  f = int(f)
766*b7c941bbSAndroid Build Coastguard Worker  assert f >= 1
767*b7c941bbSAndroid Build Coastguard Worker  h = (h//f)*f
768*b7c941bbSAndroid Build Coastguard Worker  w = (w//f)*f
769*b7c941bbSAndroid Build Coastguard Worker  img = img[0:h:, 0:w:, ::]
770*b7c941bbSAndroid Build Coastguard Worker  chs = []
771*b7c941bbSAndroid Build Coastguard Worker  for i in range(chans):
772*b7c941bbSAndroid Build Coastguard Worker    ch = img.reshape(h*w*chans)[i::chans].reshape(h, w)
773*b7c941bbSAndroid Build Coastguard Worker    ch = ch.reshape(h, w//f, f).mean(2).reshape(h, w//f)
774*b7c941bbSAndroid Build Coastguard Worker    ch = ch.T.reshape(w//f, h//f, f).mean(2).T.reshape(h//f, w//f)
775*b7c941bbSAndroid Build Coastguard Worker    chs.append(ch.reshape(h*w//(f*f)))
776*b7c941bbSAndroid Build Coastguard Worker  img = numpy.vstack(chs).T.reshape(h//f, w//f, chans)
777*b7c941bbSAndroid Build Coastguard Worker  return img
778*b7c941bbSAndroid Build Coastguard Worker
779*b7c941bbSAndroid Build Coastguard Worker
780*b7c941bbSAndroid Build Coastguard Workerdef convert_raw_to_rgb_image(r_plane, gr_plane, gb_plane, b_plane, props,
781*b7c941bbSAndroid Build Coastguard Worker                             cap_res, apply_ccm_raw_to_rgb=True):
782*b7c941bbSAndroid Build Coastguard Worker  """Convert a Bayer raw-16 image to an RGB image.
783*b7c941bbSAndroid Build Coastguard Worker
784*b7c941bbSAndroid Build Coastguard Worker  Includes some extremely rudimentary demosaicking and color processing
785*b7c941bbSAndroid Build Coastguard Worker  operations; the output of this function shouldn't be used for any image
786*b7c941bbSAndroid Build Coastguard Worker  quality analysis.
787*b7c941bbSAndroid Build Coastguard Worker
788*b7c941bbSAndroid Build Coastguard Worker  Args:
789*b7c941bbSAndroid Build Coastguard Worker   r_plane:
790*b7c941bbSAndroid Build Coastguard Worker   gr_plane:
791*b7c941bbSAndroid Build Coastguard Worker   gb_plane:
792*b7c941bbSAndroid Build Coastguard Worker   b_plane: Numpy arrays for each color plane
793*b7c941bbSAndroid Build Coastguard Worker            in the Bayer image, with pixels in the [0.0, 1.0] range.
794*b7c941bbSAndroid Build Coastguard Worker   props: Camera properties object.
795*b7c941bbSAndroid Build Coastguard Worker   cap_res: Capture result (metadata) object.
796*b7c941bbSAndroid Build Coastguard Worker   apply_ccm_raw_to_rgb: (Optional) boolean to apply color correction matrix.
797*b7c941bbSAndroid Build Coastguard Worker
798*b7c941bbSAndroid Build Coastguard Worker  Returns:
799*b7c941bbSAndroid Build Coastguard Worker   RGB float-3 image array, with pixel values in [0.0, 1.0]
800*b7c941bbSAndroid Build Coastguard Worker  """
801*b7c941bbSAndroid Build Coastguard Worker  # Values required for the RAW to RGB conversion.
802*b7c941bbSAndroid Build Coastguard Worker  assert_props_is_not_none(props)
803*b7c941bbSAndroid Build Coastguard Worker  white_level = get_white_level(props, cap_res)
804*b7c941bbSAndroid Build Coastguard Worker  gains = cap_res['android.colorCorrection.gains']
805*b7c941bbSAndroid Build Coastguard Worker  ccm = cap_res['android.colorCorrection.transform']
806*b7c941bbSAndroid Build Coastguard Worker
807*b7c941bbSAndroid Build Coastguard Worker  # Reorder black levels and gains to R,Gr,Gb,B, to match the order
808*b7c941bbSAndroid Build Coastguard Worker  # of the planes.
809*b7c941bbSAndroid Build Coastguard Worker  black_levels = get_black_levels(props, cap_res, is_quad_bayer=False)
810*b7c941bbSAndroid Build Coastguard Worker  logging.debug('dynamic black levels: %s', black_levels)
811*b7c941bbSAndroid Build Coastguard Worker  gains = get_gains_in_canonical_order(props, gains)
812*b7c941bbSAndroid Build Coastguard Worker
813*b7c941bbSAndroid Build Coastguard Worker  # Convert CCM from rational to float, as numpy arrays.
814*b7c941bbSAndroid Build Coastguard Worker  ccm = numpy.array(capture_request_utils.rational_to_float(ccm)).reshape(3, 3)
815*b7c941bbSAndroid Build Coastguard Worker
816*b7c941bbSAndroid Build Coastguard Worker  # Need to scale the image back to the full [0,1] range after subtracting
817*b7c941bbSAndroid Build Coastguard Worker  # the black level from each pixel.
818*b7c941bbSAndroid Build Coastguard Worker  scale = white_level / (white_level - max(black_levels))
819*b7c941bbSAndroid Build Coastguard Worker
820*b7c941bbSAndroid Build Coastguard Worker  # Three-channel black levels, normalized to [0,1] by white_level.
821*b7c941bbSAndroid Build Coastguard Worker  black_levels = numpy.array(
822*b7c941bbSAndroid Build Coastguard Worker      [b / white_level for b in [black_levels[i] for i in [0, 1, 3]]])
823*b7c941bbSAndroid Build Coastguard Worker
824*b7c941bbSAndroid Build Coastguard Worker  # Three-channel gains.
825*b7c941bbSAndroid Build Coastguard Worker  gains = numpy.array([gains[i] for i in [0, 1, 3]])
826*b7c941bbSAndroid Build Coastguard Worker
827*b7c941bbSAndroid Build Coastguard Worker  h, w = r_plane.shape[:2]
828*b7c941bbSAndroid Build Coastguard Worker  img = numpy.dstack([r_plane, (gr_plane + gb_plane) / 2.0, b_plane])
829*b7c941bbSAndroid Build Coastguard Worker  img = (((img.reshape(h, w, 3) - black_levels) * scale) * gains).clip(0.0, 1.0)
830*b7c941bbSAndroid Build Coastguard Worker  if apply_ccm_raw_to_rgb:
831*b7c941bbSAndroid Build Coastguard Worker    img = numpy.dot(
832*b7c941bbSAndroid Build Coastguard Worker        img.reshape(w * h, 3), ccm.T).reshape((h, w, 3)).clip(0.0, 1.0)
833*b7c941bbSAndroid Build Coastguard Worker  return img
834*b7c941bbSAndroid Build Coastguard Worker
835*b7c941bbSAndroid Build Coastguard Worker
836*b7c941bbSAndroid Build Coastguard Workerdef convert_y8_to_rgb_image(y_plane, w, h):
837*b7c941bbSAndroid Build Coastguard Worker  """Convert a Y 8-bit image to an RGB image.
838*b7c941bbSAndroid Build Coastguard Worker
839*b7c941bbSAndroid Build Coastguard Worker  Args:
840*b7c941bbSAndroid Build Coastguard Worker    y_plane: The packed 8-bit Y plane.
841*b7c941bbSAndroid Build Coastguard Worker    w: The width of the image.
842*b7c941bbSAndroid Build Coastguard Worker    h: The height of the image.
843*b7c941bbSAndroid Build Coastguard Worker
844*b7c941bbSAndroid Build Coastguard Worker  Returns:
845*b7c941bbSAndroid Build Coastguard Worker    RGB float-3 image array, with pixel values in [0.0, 1.0].
846*b7c941bbSAndroid Build Coastguard Worker  """
847*b7c941bbSAndroid Build Coastguard Worker  y3 = numpy.dstack([y_plane, y_plane, y_plane])
848*b7c941bbSAndroid Build Coastguard Worker  rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
849*b7c941bbSAndroid Build Coastguard Worker  rgb.reshape(w * h * 3)[:] = y3.reshape(w * h * 3)[:]
850*b7c941bbSAndroid Build Coastguard Worker  return rgb.astype(numpy.float32) / 255.0
851*b7c941bbSAndroid Build Coastguard Worker
852*b7c941bbSAndroid Build Coastguard Worker
853*b7c941bbSAndroid Build Coastguard Workerdef write_rgb_uint8_image(img, file_name):
854*b7c941bbSAndroid Build Coastguard Worker  """Save a uint8 numpy array image to a file.
855*b7c941bbSAndroid Build Coastguard Worker
856*b7c941bbSAndroid Build Coastguard Worker  Supported formats: PNG, JPEG, and others; see PIL docs for more.
857*b7c941bbSAndroid Build Coastguard Worker
858*b7c941bbSAndroid Build Coastguard Worker  Args:
859*b7c941bbSAndroid Build Coastguard Worker   img: numpy image array data.
860*b7c941bbSAndroid Build Coastguard Worker   file_name: path of file to save to; the extension specifies the format.
861*b7c941bbSAndroid Build Coastguard Worker  """
862*b7c941bbSAndroid Build Coastguard Worker  if img.dtype != 'uint8':
863*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f'Incorrect input type: {img.dtype}! Expected: uint8')
864*b7c941bbSAndroid Build Coastguard Worker  else:
865*b7c941bbSAndroid Build Coastguard Worker    Image.fromarray(img, 'RGB').save(file_name)
866*b7c941bbSAndroid Build Coastguard Worker
867*b7c941bbSAndroid Build Coastguard Worker
868*b7c941bbSAndroid Build Coastguard Workerdef write_image(img, fname, apply_gamma=False, is_yuv=False):
869*b7c941bbSAndroid Build Coastguard Worker  """Save a float-3 numpy array image to a file.
870*b7c941bbSAndroid Build Coastguard Worker
871*b7c941bbSAndroid Build Coastguard Worker  Supported formats: PNG, JPEG, and others; see PIL docs for more.
872*b7c941bbSAndroid Build Coastguard Worker
873*b7c941bbSAndroid Build Coastguard Worker  Image can be 3-channel, which is interpreted as RGB or YUV, or can be
874*b7c941bbSAndroid Build Coastguard Worker  1-channel, which is greyscale.
875*b7c941bbSAndroid Build Coastguard Worker
876*b7c941bbSAndroid Build Coastguard Worker  Can optionally specify that the image should be gamma-encoded prior to
877*b7c941bbSAndroid Build Coastguard Worker  writing it out; this should be done if the image contains linear pixel
878*b7c941bbSAndroid Build Coastguard Worker  values, to make the image look "normal".
879*b7c941bbSAndroid Build Coastguard Worker
880*b7c941bbSAndroid Build Coastguard Worker  Args:
881*b7c941bbSAndroid Build Coastguard Worker   img: Numpy image array data.
882*b7c941bbSAndroid Build Coastguard Worker   fname: Path of file to save to; the extension specifies the format.
883*b7c941bbSAndroid Build Coastguard Worker   apply_gamma: (Optional) apply gamma to the image prior to writing it.
884*b7c941bbSAndroid Build Coastguard Worker   is_yuv: Whether the image is in YUV format.
885*b7c941bbSAndroid Build Coastguard Worker  """
886*b7c941bbSAndroid Build Coastguard Worker  if apply_gamma:
887*b7c941bbSAndroid Build Coastguard Worker    img = apply_lut_to_image(img, DEFAULT_GAMMA_LUT)
888*b7c941bbSAndroid Build Coastguard Worker  (h, w, chans) = img.shape
889*b7c941bbSAndroid Build Coastguard Worker  if chans == 3:
890*b7c941bbSAndroid Build Coastguard Worker    if not is_yuv:
891*b7c941bbSAndroid Build Coastguard Worker      Image.fromarray((img * 255.0).astype(numpy.uint8), 'RGB').save(fname)
892*b7c941bbSAndroid Build Coastguard Worker    else:
893*b7c941bbSAndroid Build Coastguard Worker      Image.fromarray((img * 255.0).astype(numpy.uint8), 'YCbCr').save(fname)
894*b7c941bbSAndroid Build Coastguard Worker  elif chans == 1:
895*b7c941bbSAndroid Build Coastguard Worker    img3 = (img * 255.0).astype(numpy.uint8).repeat(3).reshape(h, w, 3)
896*b7c941bbSAndroid Build Coastguard Worker    Image.fromarray(img3, 'RGB').save(fname)
897*b7c941bbSAndroid Build Coastguard Worker  else:
898*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError('Unsupported image type')
899*b7c941bbSAndroid Build Coastguard Worker
900*b7c941bbSAndroid Build Coastguard Worker
901*b7c941bbSAndroid Build Coastguard Workerdef read_image(fname):
902*b7c941bbSAndroid Build Coastguard Worker  """Read image function to match write_image() above."""
903*b7c941bbSAndroid Build Coastguard Worker  return Image.open(fname)
904*b7c941bbSAndroid Build Coastguard Worker
905*b7c941bbSAndroid Build Coastguard Worker
906*b7c941bbSAndroid Build Coastguard Workerdef apply_lut_to_image(img, lut):
907*b7c941bbSAndroid Build Coastguard Worker  """Applies a LUT to every pixel in a float image array.
908*b7c941bbSAndroid Build Coastguard Worker
909*b7c941bbSAndroid Build Coastguard Worker  Internally converts to a 16b integer image, since the LUT can work with up
910*b7c941bbSAndroid Build Coastguard Worker  to 16b->16b mappings (i.e. values in the range [0,65535]). The lut can also
911*b7c941bbSAndroid Build Coastguard Worker  have fewer than 65536 entries, however it must be sized as a power of 2
912*b7c941bbSAndroid Build Coastguard Worker  (and for smaller luts, the scale must match the bitdepth).
913*b7c941bbSAndroid Build Coastguard Worker
914*b7c941bbSAndroid Build Coastguard Worker  For a 16b lut of 65536 entries, the operation performed is:
915*b7c941bbSAndroid Build Coastguard Worker
916*b7c941bbSAndroid Build Coastguard Worker  lut[r * 65535] / 65535 -> r'
917*b7c941bbSAndroid Build Coastguard Worker  lut[g * 65535] / 65535 -> g'
918*b7c941bbSAndroid Build Coastguard Worker  lut[b * 65535] / 65535 -> b'
919*b7c941bbSAndroid Build Coastguard Worker
920*b7c941bbSAndroid Build Coastguard Worker  For a 10b lut of 1024 entries, the operation becomes:
921*b7c941bbSAndroid Build Coastguard Worker
922*b7c941bbSAndroid Build Coastguard Worker  lut[r * 1023] / 1023 -> r'
923*b7c941bbSAndroid Build Coastguard Worker  lut[g * 1023] / 1023 -> g'
924*b7c941bbSAndroid Build Coastguard Worker  lut[b * 1023] / 1023 -> b'
925*b7c941bbSAndroid Build Coastguard Worker
926*b7c941bbSAndroid Build Coastguard Worker  Args:
927*b7c941bbSAndroid Build Coastguard Worker    img: Numpy float image array, with pixel values in [0,1].
928*b7c941bbSAndroid Build Coastguard Worker    lut: Numpy table encoding a LUT, mapping 16b integer values.
929*b7c941bbSAndroid Build Coastguard Worker
930*b7c941bbSAndroid Build Coastguard Worker  Returns:
931*b7c941bbSAndroid Build Coastguard Worker    Float image array after applying LUT to each pixel.
932*b7c941bbSAndroid Build Coastguard Worker  """
933*b7c941bbSAndroid Build Coastguard Worker  n = len(lut)
934*b7c941bbSAndroid Build Coastguard Worker  if n <= 0 or n > MAX_LUT_SIZE or (n & (n - 1)) != 0:
935*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError(f'Invalid arg LUT size: {n}')
936*b7c941bbSAndroid Build Coastguard Worker  m = float(n - 1)
937*b7c941bbSAndroid Build Coastguard Worker  return (lut[(img * m).astype(numpy.uint16)] / m).astype(numpy.float32)
938*b7c941bbSAndroid Build Coastguard Worker
939*b7c941bbSAndroid Build Coastguard Worker
940*b7c941bbSAndroid Build Coastguard Workerdef get_gains_in_canonical_order(props, gains):
941*b7c941bbSAndroid Build Coastguard Worker  """Reorders the gains tuple to the canonical R,Gr,Gb,B order.
942*b7c941bbSAndroid Build Coastguard Worker
943*b7c941bbSAndroid Build Coastguard Worker  Args:
944*b7c941bbSAndroid Build Coastguard Worker    props: Camera properties object.
945*b7c941bbSAndroid Build Coastguard Worker    gains: List of 4 values, in R,G_even,G_odd,B order.
946*b7c941bbSAndroid Build Coastguard Worker
947*b7c941bbSAndroid Build Coastguard Worker  Returns:
948*b7c941bbSAndroid Build Coastguard Worker    List of gains values, in R,Gr,Gb,B order.
949*b7c941bbSAndroid Build Coastguard Worker  """
950*b7c941bbSAndroid Build Coastguard Worker  cfa_pat = props['android.sensor.info.colorFilterArrangement']
951*b7c941bbSAndroid Build Coastguard Worker  if cfa_pat in [0, 1]:
952*b7c941bbSAndroid Build Coastguard Worker    # RGGB or GRBG, so G_even is Gr
953*b7c941bbSAndroid Build Coastguard Worker    return gains
954*b7c941bbSAndroid Build Coastguard Worker  elif cfa_pat in [2, 3]:
955*b7c941bbSAndroid Build Coastguard Worker    # GBRG or BGGR, so G_even is Gb
956*b7c941bbSAndroid Build Coastguard Worker    return [gains[0], gains[2], gains[1], gains[3]]
957*b7c941bbSAndroid Build Coastguard Worker  else:
958*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError('Not supported')
959*b7c941bbSAndroid Build Coastguard Worker
960*b7c941bbSAndroid Build Coastguard Worker
961*b7c941bbSAndroid Build Coastguard Workerdef get_white_level(props, cap_metadata=None):
962*b7c941bbSAndroid Build Coastguard Worker  """Gets white level to use for a given capture.
963*b7c941bbSAndroid Build Coastguard Worker
964*b7c941bbSAndroid Build Coastguard Worker  Uses a dynamic value from the capture result if available, else falls back
965*b7c941bbSAndroid Build Coastguard Worker  to the static global value in the camera characteristics.
966*b7c941bbSAndroid Build Coastguard Worker
967*b7c941bbSAndroid Build Coastguard Worker  Args:
968*b7c941bbSAndroid Build Coastguard Worker    props: The camera properties object.
969*b7c941bbSAndroid Build Coastguard Worker    cap_metadata: A capture results metadata object.
970*b7c941bbSAndroid Build Coastguard Worker
971*b7c941bbSAndroid Build Coastguard Worker  Returns:
972*b7c941bbSAndroid Build Coastguard Worker    Float white level value.
973*b7c941bbSAndroid Build Coastguard Worker  """
974*b7c941bbSAndroid Build Coastguard Worker  if (cap_metadata is not None and
975*b7c941bbSAndroid Build Coastguard Worker      'android.sensor.dynamicWhiteLevel' in cap_metadata and
976*b7c941bbSAndroid Build Coastguard Worker      cap_metadata['android.sensor.dynamicWhiteLevel'] is not None):
977*b7c941bbSAndroid Build Coastguard Worker    white_level = cap_metadata['android.sensor.dynamicWhiteLevel']
978*b7c941bbSAndroid Build Coastguard Worker    logging.debug('dynamic white level: %.2f', white_level)
979*b7c941bbSAndroid Build Coastguard Worker  else:
980*b7c941bbSAndroid Build Coastguard Worker    white_level = props['android.sensor.info.whiteLevel']
981*b7c941bbSAndroid Build Coastguard Worker    logging.debug('white level: %.2f', white_level)
982*b7c941bbSAndroid Build Coastguard Worker  return float(white_level)
983*b7c941bbSAndroid Build Coastguard Worker
984*b7c941bbSAndroid Build Coastguard Worker
985*b7c941bbSAndroid Build Coastguard Workerdef get_black_levels(props, cap=None, is_quad_bayer=False):
986*b7c941bbSAndroid Build Coastguard Worker  """Gets black levels to use for a given capture.
987*b7c941bbSAndroid Build Coastguard Worker
988*b7c941bbSAndroid Build Coastguard Worker  Uses a dynamic value from the capture result if available, else falls back
989*b7c941bbSAndroid Build Coastguard Worker  to the static global value in the camera characteristics.
990*b7c941bbSAndroid Build Coastguard Worker
991*b7c941bbSAndroid Build Coastguard Worker  Args:
992*b7c941bbSAndroid Build Coastguard Worker    props: The camera properties object.
993*b7c941bbSAndroid Build Coastguard Worker    cap: A capture object.
994*b7c941bbSAndroid Build Coastguard Worker    is_quad_bayer: Boolean flag for Bayer or Quad Bayer capture.
995*b7c941bbSAndroid Build Coastguard Worker
996*b7c941bbSAndroid Build Coastguard Worker  Returns:
997*b7c941bbSAndroid Build Coastguard Worker    A list of black level values reordered in canonical order.
998*b7c941bbSAndroid Build Coastguard Worker  """
999*b7c941bbSAndroid Build Coastguard Worker  if (cap is not None and
1000*b7c941bbSAndroid Build Coastguard Worker      'android.sensor.dynamicBlackLevel' in cap and
1001*b7c941bbSAndroid Build Coastguard Worker      cap['android.sensor.dynamicBlackLevel'] is not None):
1002*b7c941bbSAndroid Build Coastguard Worker    black_levels = cap['android.sensor.dynamicBlackLevel']
1003*b7c941bbSAndroid Build Coastguard Worker  else:
1004*b7c941bbSAndroid Build Coastguard Worker    black_levels = props['android.sensor.blackLevelPattern']
1005*b7c941bbSAndroid Build Coastguard Worker
1006*b7c941bbSAndroid Build Coastguard Worker  idxs = get_canonical_cfa_order(props, is_quad_bayer)
1007*b7c941bbSAndroid Build Coastguard Worker  if is_quad_bayer:
1008*b7c941bbSAndroid Build Coastguard Worker    ordered_black_levels = [black_levels[i // 4] for i in idxs]
1009*b7c941bbSAndroid Build Coastguard Worker  else:
1010*b7c941bbSAndroid Build Coastguard Worker    ordered_black_levels = [black_levels[i] for i in idxs]
1011*b7c941bbSAndroid Build Coastguard Worker  return ordered_black_levels
1012*b7c941bbSAndroid Build Coastguard Worker
1013*b7c941bbSAndroid Build Coastguard Worker
1014*b7c941bbSAndroid Build Coastguard Workerdef get_canonical_cfa_order(props, is_quad_bayer=False):
1015*b7c941bbSAndroid Build Coastguard Worker  """Returns a list of channel indices according to color filter arrangement.
1016*b7c941bbSAndroid Build Coastguard Worker
1017*b7c941bbSAndroid Build Coastguard Worker  Color filter arrangement index is a integer ranging from 0 to 3, which maps
1018*b7c941bbSAndroid Build Coastguard Worker  the color filter arrangement in the following way.
1019*b7c941bbSAndroid Build Coastguard Worker    0: R, Gr, Gb, B,
1020*b7c941bbSAndroid Build Coastguard Worker    1: Gr, R, B, Gb,
1021*b7c941bbSAndroid Build Coastguard Worker    2: Gb, B, R, Gr,
1022*b7c941bbSAndroid Build Coastguard Worker    3: B, Gb, Gr, R.
1023*b7c941bbSAndroid Build Coastguard Worker
1024*b7c941bbSAndroid Build Coastguard Worker  This function return a list of channel indices that can be used to reorder
1025*b7c941bbSAndroid Build Coastguard Worker  the stats data as the canonical order:
1026*b7c941bbSAndroid Build Coastguard Worker    (1) For standard Bayer: R, Gr, Gb, B.
1027*b7c941bbSAndroid Build Coastguard Worker    (2) For quad Bayer: R0, R1, R2, R3,
1028*b7c941bbSAndroid Build Coastguard Worker                        Gr0, Gr1, Gr2, Gr3,
1029*b7c941bbSAndroid Build Coastguard Worker                        Gb0, Gb1, Gb2, Gb3,
1030*b7c941bbSAndroid Build Coastguard Worker                        B0, B1, B2, B3.
1031*b7c941bbSAndroid Build Coastguard Worker
1032*b7c941bbSAndroid Build Coastguard Worker  Args:
1033*b7c941bbSAndroid Build Coastguard Worker    props: Camera properties object.
1034*b7c941bbSAndroid Build Coastguard Worker    is_quad_bayer: Boolean flag for Bayer or Quad Bayer capture.
1035*b7c941bbSAndroid Build Coastguard Worker
1036*b7c941bbSAndroid Build Coastguard Worker  Returns:
1037*b7c941bbSAndroid Build Coastguard Worker    A list of channel indices with values ranging from:
1038*b7c941bbSAndroid Build Coastguard Worker      (1) [0, 3] for standard Bayer,
1039*b7c941bbSAndroid Build Coastguard Worker      (2) [0, 15] for quad Bayer.
1040*b7c941bbSAndroid Build Coastguard Worker  """
1041*b7c941bbSAndroid Build Coastguard Worker  cfa_pat = props['android.sensor.info.colorFilterArrangement']
1042*b7c941bbSAndroid Build Coastguard Worker  if not 0 <= cfa_pat < 4:
1043*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError('Not supported')
1044*b7c941bbSAndroid Build Coastguard Worker
1045*b7c941bbSAndroid Build Coastguard Worker  channel_indices = []
1046*b7c941bbSAndroid Build Coastguard Worker  if is_quad_bayer:
1047*b7c941bbSAndroid Build Coastguard Worker    color_map = noise_model_constants.QUAD_BAYER_COLOR_FILTER_MAP[cfa_pat]
1048*b7c941bbSAndroid Build Coastguard Worker    for ch in noise_model_constants.BAYER_COLORS:
1049*b7c941bbSAndroid Build Coastguard Worker      channel_indices.extend(color_map[ch])
1050*b7c941bbSAndroid Build Coastguard Worker  else:
1051*b7c941bbSAndroid Build Coastguard Worker    color_map = noise_model_constants.BAYER_COLOR_FILTER_MAP[cfa_pat]
1052*b7c941bbSAndroid Build Coastguard Worker    channel_indices = [
1053*b7c941bbSAndroid Build Coastguard Worker        color_map[ch] for ch in noise_model_constants.BAYER_COLORS
1054*b7c941bbSAndroid Build Coastguard Worker    ]
1055*b7c941bbSAndroid Build Coastguard Worker  return channel_indices
1056*b7c941bbSAndroid Build Coastguard Worker
1057*b7c941bbSAndroid Build Coastguard Worker
1058*b7c941bbSAndroid Build Coastguard Workerdef unpack_rawstats_capture(cap, num_channels=4):
1059*b7c941bbSAndroid Build Coastguard Worker  """Unpacks a stats image capture to the mean and variance images.
1060*b7c941bbSAndroid Build Coastguard Worker
1061*b7c941bbSAndroid Build Coastguard Worker  Args:
1062*b7c941bbSAndroid Build Coastguard Worker    cap: A capture object as returned by its_session_utils.do_capture.
1063*b7c941bbSAndroid Build Coastguard Worker    num_channels: The number of color channels in the stats image capture, which
1064*b7c941bbSAndroid Build Coastguard Worker      can be one of noise_model_constants.VALID_NUM_CHANNELS.
1065*b7c941bbSAndroid Build Coastguard Worker
1066*b7c941bbSAndroid Build Coastguard Worker  Returns:
1067*b7c941bbSAndroid Build Coastguard Worker    Tuple (mean_image var_image) of float-4 images, with non-normalized
1068*b7c941bbSAndroid Build Coastguard Worker    pixel values computed from the RAW10/RAW16 images on the device
1069*b7c941bbSAndroid Build Coastguard Worker  """
1070*b7c941bbSAndroid Build Coastguard Worker  if cap['format'] not in noise_model_constants.VALID_RAW_STATS_FORMATS:
1071*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f"Unsupported stats format: {cap['format']}")
1072*b7c941bbSAndroid Build Coastguard Worker
1073*b7c941bbSAndroid Build Coastguard Worker  if num_channels not in noise_model_constants.VALID_NUM_CHANNELS:
1074*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
1075*b7c941bbSAndroid Build Coastguard Worker        f'Unsupported number of channels {num_channels}, which should be in'
1076*b7c941bbSAndroid Build Coastguard Worker        f' {noise_model_constants.VALID_NUM_CHANNELS}.'
1077*b7c941bbSAndroid Build Coastguard Worker    )
1078*b7c941bbSAndroid Build Coastguard Worker
1079*b7c941bbSAndroid Build Coastguard Worker  w = cap['width']
1080*b7c941bbSAndroid Build Coastguard Worker  h = cap['height']
1081*b7c941bbSAndroid Build Coastguard Worker  img = numpy.ndarray(
1082*b7c941bbSAndroid Build Coastguard Worker      shape=(2 * h * w * num_channels,), dtype='<f', buffer=cap['data']
1083*b7c941bbSAndroid Build Coastguard Worker  )
1084*b7c941bbSAndroid Build Coastguard Worker  analysis_image = img.reshape((2, h, w, num_channels))
1085*b7c941bbSAndroid Build Coastguard Worker  mean_image = analysis_image[0, :, :, :].reshape(h, w, num_channels)
1086*b7c941bbSAndroid Build Coastguard Worker  var_image = analysis_image[1, :, :, :].reshape(h, w, num_channels)
1087*b7c941bbSAndroid Build Coastguard Worker  return mean_image, var_image
1088*b7c941bbSAndroid Build Coastguard Worker
1089*b7c941bbSAndroid Build Coastguard Worker
1090*b7c941bbSAndroid Build Coastguard Workerdef get_image_patch(img, xnorm, ynorm, wnorm, hnorm):
1091*b7c941bbSAndroid Build Coastguard Worker  """Get a patch (tile) of an image.
1092*b7c941bbSAndroid Build Coastguard Worker
1093*b7c941bbSAndroid Build Coastguard Worker  Args:
1094*b7c941bbSAndroid Build Coastguard Worker   img: Numpy float image array, with pixel values in [0,1].
1095*b7c941bbSAndroid Build Coastguard Worker   xnorm:
1096*b7c941bbSAndroid Build Coastguard Worker   ynorm:
1097*b7c941bbSAndroid Build Coastguard Worker   wnorm:
1098*b7c941bbSAndroid Build Coastguard Worker   hnorm: Normalized (in [0,1]) coords for the tile.
1099*b7c941bbSAndroid Build Coastguard Worker
1100*b7c941bbSAndroid Build Coastguard Worker  Returns:
1101*b7c941bbSAndroid Build Coastguard Worker     Numpy float image array of the patch.
1102*b7c941bbSAndroid Build Coastguard Worker  """
1103*b7c941bbSAndroid Build Coastguard Worker  hfull = img.shape[0]
1104*b7c941bbSAndroid Build Coastguard Worker  wfull = img.shape[1]
1105*b7c941bbSAndroid Build Coastguard Worker  xtile = int(math.ceil(xnorm * wfull))
1106*b7c941bbSAndroid Build Coastguard Worker  ytile = int(math.ceil(ynorm * hfull))
1107*b7c941bbSAndroid Build Coastguard Worker  wtile = int(math.floor(wnorm * wfull))
1108*b7c941bbSAndroid Build Coastguard Worker  htile = int(math.floor(hnorm * hfull))
1109*b7c941bbSAndroid Build Coastguard Worker  if len(img.shape) == 2:
1110*b7c941bbSAndroid Build Coastguard Worker    return img[ytile:ytile + htile, xtile:xtile + wtile].copy()
1111*b7c941bbSAndroid Build Coastguard Worker  else:
1112*b7c941bbSAndroid Build Coastguard Worker    return img[ytile:ytile + htile, xtile:xtile + wtile, :].copy()
1113*b7c941bbSAndroid Build Coastguard Worker
1114*b7c941bbSAndroid Build Coastguard Worker
1115*b7c941bbSAndroid Build Coastguard Workerdef compute_image_means(img):
1116*b7c941bbSAndroid Build Coastguard Worker  """Calculate the mean of each color channel in the image.
1117*b7c941bbSAndroid Build Coastguard Worker
1118*b7c941bbSAndroid Build Coastguard Worker  Args:
1119*b7c941bbSAndroid Build Coastguard Worker    img: Numpy float image array, with pixel values in [0,1].
1120*b7c941bbSAndroid Build Coastguard Worker
1121*b7c941bbSAndroid Build Coastguard Worker  Returns:
1122*b7c941bbSAndroid Build Coastguard Worker     A list of mean values, one per color channel in the image.
1123*b7c941bbSAndroid Build Coastguard Worker  """
1124*b7c941bbSAndroid Build Coastguard Worker  means = []
1125*b7c941bbSAndroid Build Coastguard Worker  chans = img.shape[2]
1126*b7c941bbSAndroid Build Coastguard Worker  for i in range(chans):
1127*b7c941bbSAndroid Build Coastguard Worker    means.append(numpy.mean(img[:, :, i], dtype=numpy.float64))
1128*b7c941bbSAndroid Build Coastguard Worker  return means
1129*b7c941bbSAndroid Build Coastguard Worker
1130*b7c941bbSAndroid Build Coastguard Worker
1131*b7c941bbSAndroid Build Coastguard Workerdef compute_image_variances(img):
1132*b7c941bbSAndroid Build Coastguard Worker  """Calculate the variance of each color channel in the image.
1133*b7c941bbSAndroid Build Coastguard Worker
1134*b7c941bbSAndroid Build Coastguard Worker  Args:
1135*b7c941bbSAndroid Build Coastguard Worker    img: Numpy float image array, with pixel values in [0,1].
1136*b7c941bbSAndroid Build Coastguard Worker
1137*b7c941bbSAndroid Build Coastguard Worker  Returns:
1138*b7c941bbSAndroid Build Coastguard Worker    A list of variance values, one per color channel in the image.
1139*b7c941bbSAndroid Build Coastguard Worker  """
1140*b7c941bbSAndroid Build Coastguard Worker  variances = []
1141*b7c941bbSAndroid Build Coastguard Worker  chans = img.shape[2]
1142*b7c941bbSAndroid Build Coastguard Worker  for i in range(chans):
1143*b7c941bbSAndroid Build Coastguard Worker    variances.append(numpy.var(img[:, :, i], dtype=numpy.float64))
1144*b7c941bbSAndroid Build Coastguard Worker  return variances
1145*b7c941bbSAndroid Build Coastguard Worker
1146*b7c941bbSAndroid Build Coastguard Worker
1147*b7c941bbSAndroid Build Coastguard Workerdef compute_image_sharpness(img):
1148*b7c941bbSAndroid Build Coastguard Worker  """Calculate the sharpness of input image.
1149*b7c941bbSAndroid Build Coastguard Worker
1150*b7c941bbSAndroid Build Coastguard Worker  Args:
1151*b7c941bbSAndroid Build Coastguard Worker    img: numpy float RGB/luma image array, with pixel values in [0,1].
1152*b7c941bbSAndroid Build Coastguard Worker
1153*b7c941bbSAndroid Build Coastguard Worker  Returns:
1154*b7c941bbSAndroid Build Coastguard Worker    Sharpness estimation value based on the average of gradient magnitude.
1155*b7c941bbSAndroid Build Coastguard Worker    Larger value means the image is sharper.
1156*b7c941bbSAndroid Build Coastguard Worker  """
1157*b7c941bbSAndroid Build Coastguard Worker  chans = img.shape[2]
1158*b7c941bbSAndroid Build Coastguard Worker  if chans != 1 and chans != 3:
1159*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f'Not RGB or MONO image! depth: {chans}')
1160*b7c941bbSAndroid Build Coastguard Worker  if chans == 1:
1161*b7c941bbSAndroid Build Coastguard Worker    luma = img[:, :, 0]
1162*b7c941bbSAndroid Build Coastguard Worker  else:
1163*b7c941bbSAndroid Build Coastguard Worker    luma = convert_rgb_to_grayscale(img)
1164*b7c941bbSAndroid Build Coastguard Worker  gy, gx = numpy.gradient(luma)
1165*b7c941bbSAndroid Build Coastguard Worker  return numpy.average(numpy.sqrt(gy*gy + gx*gx))
1166*b7c941bbSAndroid Build Coastguard Worker
1167*b7c941bbSAndroid Build Coastguard Worker
1168*b7c941bbSAndroid Build Coastguard Workerdef compute_image_max_gradients(img):
1169*b7c941bbSAndroid Build Coastguard Worker  """Calculate the maximum gradient of each color channel in the image.
1170*b7c941bbSAndroid Build Coastguard Worker
1171*b7c941bbSAndroid Build Coastguard Worker  Args:
1172*b7c941bbSAndroid Build Coastguard Worker    img: Numpy float image array, with pixel values in [0,1].
1173*b7c941bbSAndroid Build Coastguard Worker
1174*b7c941bbSAndroid Build Coastguard Worker  Returns:
1175*b7c941bbSAndroid Build Coastguard Worker    A list of gradient max values, one per color channel in the image.
1176*b7c941bbSAndroid Build Coastguard Worker  """
1177*b7c941bbSAndroid Build Coastguard Worker  grads = []
1178*b7c941bbSAndroid Build Coastguard Worker  chans = img.shape[2]
1179*b7c941bbSAndroid Build Coastguard Worker  for i in range(chans):
1180*b7c941bbSAndroid Build Coastguard Worker    grads.append(numpy.amax(numpy.gradient(img[:, :, i])))
1181*b7c941bbSAndroid Build Coastguard Worker  return grads
1182*b7c941bbSAndroid Build Coastguard Worker
1183*b7c941bbSAndroid Build Coastguard Worker
1184*b7c941bbSAndroid Build Coastguard Workerdef compute_image_snrs(img):
1185*b7c941bbSAndroid Build Coastguard Worker  """Calculate the SNR (dB) of each color channel in the image.
1186*b7c941bbSAndroid Build Coastguard Worker
1187*b7c941bbSAndroid Build Coastguard Worker  Args:
1188*b7c941bbSAndroid Build Coastguard Worker    img: Numpy float image array, with pixel values in [0,1].
1189*b7c941bbSAndroid Build Coastguard Worker
1190*b7c941bbSAndroid Build Coastguard Worker  Returns:
1191*b7c941bbSAndroid Build Coastguard Worker    A list of SNR values in dB, one per color channel in the image.
1192*b7c941bbSAndroid Build Coastguard Worker  """
1193*b7c941bbSAndroid Build Coastguard Worker  means = compute_image_means(img)
1194*b7c941bbSAndroid Build Coastguard Worker  variances = compute_image_variances(img)
1195*b7c941bbSAndroid Build Coastguard Worker  std_devs = [math.sqrt(v) for v in variances]
1196*b7c941bbSAndroid Build Coastguard Worker  snrs = [20 * math.log10(m/s) for m, s in zip(means, std_devs)]
1197*b7c941bbSAndroid Build Coastguard Worker  return snrs
1198*b7c941bbSAndroid Build Coastguard Worker
1199*b7c941bbSAndroid Build Coastguard Worker
1200*b7c941bbSAndroid Build Coastguard Workerdef convert_rgb_to_grayscale(img):
1201*b7c941bbSAndroid Build Coastguard Worker  """Convert a 3-D array RGB image to grayscale image.
1202*b7c941bbSAndroid Build Coastguard Worker
1203*b7c941bbSAndroid Build Coastguard Worker  Args:
1204*b7c941bbSAndroid Build Coastguard Worker    img: numpy 3-D array RGB image of type [0.0, 1.0] float or [0, 255] uint8.
1205*b7c941bbSAndroid Build Coastguard Worker
1206*b7c941bbSAndroid Build Coastguard Worker  Returns:
1207*b7c941bbSAndroid Build Coastguard Worker    2-D grayscale image of same type as input.
1208*b7c941bbSAndroid Build Coastguard Worker  """
1209*b7c941bbSAndroid Build Coastguard Worker  chans = img.shape[2]
1210*b7c941bbSAndroid Build Coastguard Worker  if chans != 3:
1211*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f'Not an RGB image! Depth: {chans}')
1212*b7c941bbSAndroid Build Coastguard Worker  img_gray = numpy.dot(img[..., :3], RGB2GRAY_WEIGHTS)
1213*b7c941bbSAndroid Build Coastguard Worker  if img.dtype == 'uint8':
1214*b7c941bbSAndroid Build Coastguard Worker    return img_gray.round().astype(numpy.uint8)
1215*b7c941bbSAndroid Build Coastguard Worker  else:
1216*b7c941bbSAndroid Build Coastguard Worker    return img_gray
1217*b7c941bbSAndroid Build Coastguard Worker
1218*b7c941bbSAndroid Build Coastguard Worker
1219*b7c941bbSAndroid Build Coastguard Workerdef normalize_img(img):
1220*b7c941bbSAndroid Build Coastguard Worker  """Normalize the image values to between 0 and 1.
1221*b7c941bbSAndroid Build Coastguard Worker
1222*b7c941bbSAndroid Build Coastguard Worker  Args:
1223*b7c941bbSAndroid Build Coastguard Worker    img: 2-D numpy array of image values
1224*b7c941bbSAndroid Build Coastguard Worker  Returns:
1225*b7c941bbSAndroid Build Coastguard Worker    Normalized image
1226*b7c941bbSAndroid Build Coastguard Worker  """
1227*b7c941bbSAndroid Build Coastguard Worker  return (img - numpy.amin(img))/(numpy.amax(img) - numpy.amin(img))
1228*b7c941bbSAndroid Build Coastguard Worker
1229*b7c941bbSAndroid Build Coastguard Worker
1230*b7c941bbSAndroid Build Coastguard Workerdef rotate_img_per_argv(img):
1231*b7c941bbSAndroid Build Coastguard Worker  """Rotate an image 180 degrees if "rotate" is in argv.
1232*b7c941bbSAndroid Build Coastguard Worker
1233*b7c941bbSAndroid Build Coastguard Worker  Args:
1234*b7c941bbSAndroid Build Coastguard Worker    img: 2-D numpy array of image values
1235*b7c941bbSAndroid Build Coastguard Worker  Returns:
1236*b7c941bbSAndroid Build Coastguard Worker    Rotated image
1237*b7c941bbSAndroid Build Coastguard Worker  """
1238*b7c941bbSAndroid Build Coastguard Worker  img_out = img
1239*b7c941bbSAndroid Build Coastguard Worker  if 'rotate180' in sys.argv:
1240*b7c941bbSAndroid Build Coastguard Worker    img_out = numpy.fliplr(numpy.flipud(img_out))
1241*b7c941bbSAndroid Build Coastguard Worker  return img_out
1242*b7c941bbSAndroid Build Coastguard Worker
1243*b7c941bbSAndroid Build Coastguard Worker
1244*b7c941bbSAndroid Build Coastguard Workerdef compute_image_rms_difference_1d(rgb_x, rgb_y):
1245*b7c941bbSAndroid Build Coastguard Worker  """Calculate the RMS difference between 2 RBG images as 1D arrays.
1246*b7c941bbSAndroid Build Coastguard Worker
1247*b7c941bbSAndroid Build Coastguard Worker  Args:
1248*b7c941bbSAndroid Build Coastguard Worker    rgb_x: image array
1249*b7c941bbSAndroid Build Coastguard Worker    rgb_y: image array
1250*b7c941bbSAndroid Build Coastguard Worker
1251*b7c941bbSAndroid Build Coastguard Worker  Returns:
1252*b7c941bbSAndroid Build Coastguard Worker    rms_diff
1253*b7c941bbSAndroid Build Coastguard Worker  """
1254*b7c941bbSAndroid Build Coastguard Worker  len_rgb_x = len(rgb_x)
1255*b7c941bbSAndroid Build Coastguard Worker  len_rgb_y = len(rgb_y)
1256*b7c941bbSAndroid Build Coastguard Worker  if len_rgb_y != len_rgb_x:
1257*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('RGB images have different number of planes! '
1258*b7c941bbSAndroid Build Coastguard Worker                         f'x: {len_rgb_x}, y: {len_rgb_y}')
1259*b7c941bbSAndroid Build Coastguard Worker  return math.sqrt(sum([pow(rgb_x[i] - rgb_y[i], 2.0)
1260*b7c941bbSAndroid Build Coastguard Worker                        for i in range(len_rgb_x)]) / len_rgb_x)
1261*b7c941bbSAndroid Build Coastguard Worker
1262*b7c941bbSAndroid Build Coastguard Worker
1263*b7c941bbSAndroid Build Coastguard Workerdef compute_image_rms_difference_3d(rgb_x, rgb_y):
1264*b7c941bbSAndroid Build Coastguard Worker  """Calculate the RMS difference between 2 RBG images as 3D arrays.
1265*b7c941bbSAndroid Build Coastguard Worker
1266*b7c941bbSAndroid Build Coastguard Worker  Args:
1267*b7c941bbSAndroid Build Coastguard Worker    rgb_x: image array in the form of w * h * channels
1268*b7c941bbSAndroid Build Coastguard Worker    rgb_y: image array in the form of w * h * channels
1269*b7c941bbSAndroid Build Coastguard Worker
1270*b7c941bbSAndroid Build Coastguard Worker  Returns:
1271*b7c941bbSAndroid Build Coastguard Worker    rms_diff
1272*b7c941bbSAndroid Build Coastguard Worker  """
1273*b7c941bbSAndroid Build Coastguard Worker  shape_rgb_x = numpy.shape(rgb_x)
1274*b7c941bbSAndroid Build Coastguard Worker  shape_rgb_y = numpy.shape(rgb_y)
1275*b7c941bbSAndroid Build Coastguard Worker  if shape_rgb_y != shape_rgb_x:
1276*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('RGB images have different number of planes! '
1277*b7c941bbSAndroid Build Coastguard Worker                         f'x: {shape_rgb_x}, y: {shape_rgb_y}')
1278*b7c941bbSAndroid Build Coastguard Worker  if len(shape_rgb_x) != 3:
1279*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f'RGB images dimension {len(shape_rgb_x)} is not 3!')
1280*b7c941bbSAndroid Build Coastguard Worker
1281*b7c941bbSAndroid Build Coastguard Worker  mean_square_sum = 0.0
1282*b7c941bbSAndroid Build Coastguard Worker  for i in range(shape_rgb_x[0]):
1283*b7c941bbSAndroid Build Coastguard Worker    for j in range(shape_rgb_x[1]):
1284*b7c941bbSAndroid Build Coastguard Worker      for k in range(shape_rgb_x[2]):
1285*b7c941bbSAndroid Build Coastguard Worker        mean_square_sum += pow(float(rgb_x[i][j][k]) - float(rgb_y[i][j][k]),
1286*b7c941bbSAndroid Build Coastguard Worker                               2.0)
1287*b7c941bbSAndroid Build Coastguard Worker  return (math.sqrt(mean_square_sum /
1288*b7c941bbSAndroid Build Coastguard Worker                    (shape_rgb_x[0] * shape_rgb_x[1] * shape_rgb_x[2])))
1289*b7c941bbSAndroid Build Coastguard Worker
1290*b7c941bbSAndroid Build Coastguard Worker
1291*b7c941bbSAndroid Build Coastguard Workerdef compute_image_sad(img_x, img_y):
1292*b7c941bbSAndroid Build Coastguard Worker  """Calculate the sum of absolute differences between 2 images.
1293*b7c941bbSAndroid Build Coastguard Worker
1294*b7c941bbSAndroid Build Coastguard Worker  Args:
1295*b7c941bbSAndroid Build Coastguard Worker    img_x: image array in the form of w * h * channels
1296*b7c941bbSAndroid Build Coastguard Worker    img_y: image array in the form of w * h * channels
1297*b7c941bbSAndroid Build Coastguard Worker
1298*b7c941bbSAndroid Build Coastguard Worker  Returns:
1299*b7c941bbSAndroid Build Coastguard Worker    sad
1300*b7c941bbSAndroid Build Coastguard Worker  """
1301*b7c941bbSAndroid Build Coastguard Worker  img_x = img_x[:, :, 1:].ravel()
1302*b7c941bbSAndroid Build Coastguard Worker  img_y = img_y[:, :, 1:].ravel()
1303*b7c941bbSAndroid Build Coastguard Worker  return numpy.sum(numpy.abs(numpy.subtract(img_x, img_y, dtype=float)))
1304*b7c941bbSAndroid Build Coastguard Worker
1305*b7c941bbSAndroid Build Coastguard Worker
1306*b7c941bbSAndroid Build Coastguard Workerdef get_img(buffer):
1307*b7c941bbSAndroid Build Coastguard Worker  """Return a PIL.Image of the capture buffer.
1308*b7c941bbSAndroid Build Coastguard Worker
1309*b7c941bbSAndroid Build Coastguard Worker  Args:
1310*b7c941bbSAndroid Build Coastguard Worker    buffer: data field from the capture result.
1311*b7c941bbSAndroid Build Coastguard Worker
1312*b7c941bbSAndroid Build Coastguard Worker  Returns:
1313*b7c941bbSAndroid Build Coastguard Worker    A PIL.Image
1314*b7c941bbSAndroid Build Coastguard Worker  """
1315*b7c941bbSAndroid Build Coastguard Worker  return Image.open(io.BytesIO(buffer))
1316*b7c941bbSAndroid Build Coastguard Worker
1317*b7c941bbSAndroid Build Coastguard Worker
1318*b7c941bbSAndroid Build Coastguard Workerdef jpeg_has_icc_profile(jpeg_img):
1319*b7c941bbSAndroid Build Coastguard Worker  """Checks if a jpeg PIL.Image has an icc profile attached.
1320*b7c941bbSAndroid Build Coastguard Worker
1321*b7c941bbSAndroid Build Coastguard Worker  Args:
1322*b7c941bbSAndroid Build Coastguard Worker    jpeg_img: The PIL.Image.
1323*b7c941bbSAndroid Build Coastguard Worker
1324*b7c941bbSAndroid Build Coastguard Worker  Returns:
1325*b7c941bbSAndroid Build Coastguard Worker    True if an icc profile is present, False otherwise.
1326*b7c941bbSAndroid Build Coastguard Worker  """
1327*b7c941bbSAndroid Build Coastguard Worker  return jpeg_img.info.get('icc_profile') is not None
1328*b7c941bbSAndroid Build Coastguard Worker
1329*b7c941bbSAndroid Build Coastguard Worker
1330*b7c941bbSAndroid Build Coastguard Workerdef get_primary_chromaticity(primary):
1331*b7c941bbSAndroid Build Coastguard Worker  """Given an ImageCms primary, returns just the xy chromaticity coordinates.
1332*b7c941bbSAndroid Build Coastguard Worker
1333*b7c941bbSAndroid Build Coastguard Worker  Args:
1334*b7c941bbSAndroid Build Coastguard Worker    primary: The primary from the ImageCms profile.
1335*b7c941bbSAndroid Build Coastguard Worker
1336*b7c941bbSAndroid Build Coastguard Worker  Returns:
1337*b7c941bbSAndroid Build Coastguard Worker    (float, float): The xy chromaticity coordinates of the primary.
1338*b7c941bbSAndroid Build Coastguard Worker  """
1339*b7c941bbSAndroid Build Coastguard Worker  ((_, _, _), (x, y, _)) = primary
1340*b7c941bbSAndroid Build Coastguard Worker  return x, y
1341*b7c941bbSAndroid Build Coastguard Worker
1342*b7c941bbSAndroid Build Coastguard Worker
1343*b7c941bbSAndroid Build Coastguard Workerdef is_jpeg_icc_profile_correct(jpeg_img, color_space, icc_profile_path=None):
1344*b7c941bbSAndroid Build Coastguard Worker  """Compare a jpeg's icc profile to a color space's expected parameters.
1345*b7c941bbSAndroid Build Coastguard Worker
1346*b7c941bbSAndroid Build Coastguard Worker  Args:
1347*b7c941bbSAndroid Build Coastguard Worker    jpeg_img: The PIL.Image.
1348*b7c941bbSAndroid Build Coastguard Worker    color_space: 'DISPLAY_P3' or 'SRGB'
1349*b7c941bbSAndroid Build Coastguard Worker    icc_profile_path: Optional path to an icc file to be created with the
1350*b7c941bbSAndroid Build Coastguard Worker        raw contents.
1351*b7c941bbSAndroid Build Coastguard Worker
1352*b7c941bbSAndroid Build Coastguard Worker  Returns:
1353*b7c941bbSAndroid Build Coastguard Worker    True if the icc profile matches expectations, False otherwise.
1354*b7c941bbSAndroid Build Coastguard Worker  """
1355*b7c941bbSAndroid Build Coastguard Worker  icc = jpeg_img.info.get('icc_profile')
1356*b7c941bbSAndroid Build Coastguard Worker  f = io.BytesIO(icc)
1357*b7c941bbSAndroid Build Coastguard Worker  icc_profile = ImageCms.getOpenProfile(f)
1358*b7c941bbSAndroid Build Coastguard Worker
1359*b7c941bbSAndroid Build Coastguard Worker  if icc_profile_path is not None:
1360*b7c941bbSAndroid Build Coastguard Worker    raw_icc_bytes = f.getvalue()
1361*b7c941bbSAndroid Build Coastguard Worker    f = open(icc_profile_path, 'wb')
1362*b7c941bbSAndroid Build Coastguard Worker    f.write(raw_icc_bytes)
1363*b7c941bbSAndroid Build Coastguard Worker    f.close()
1364*b7c941bbSAndroid Build Coastguard Worker
1365*b7c941bbSAndroid Build Coastguard Worker  cms_profile = icc_profile.profile
1366*b7c941bbSAndroid Build Coastguard Worker  (rx, ry) = get_primary_chromaticity(cms_profile.red_primary)
1367*b7c941bbSAndroid Build Coastguard Worker  (gx, gy) = get_primary_chromaticity(cms_profile.green_primary)
1368*b7c941bbSAndroid Build Coastguard Worker  (bx, by) = get_primary_chromaticity(cms_profile.blue_primary)
1369*b7c941bbSAndroid Build Coastguard Worker
1370*b7c941bbSAndroid Build Coastguard Worker  if color_space == 'DISPLAY_P3':
1371*b7c941bbSAndroid Build Coastguard Worker    # Expected primaries based on Apple's Display P3 primaries
1372*b7c941bbSAndroid Build Coastguard Worker    expected_rx = EXPECTED_RX_P3
1373*b7c941bbSAndroid Build Coastguard Worker    expected_ry = EXPECTED_RY_P3
1374*b7c941bbSAndroid Build Coastguard Worker    expected_gx = EXPECTED_GX_P3
1375*b7c941bbSAndroid Build Coastguard Worker    expected_gy = EXPECTED_GY_P3
1376*b7c941bbSAndroid Build Coastguard Worker    expected_bx = EXPECTED_BX_P3
1377*b7c941bbSAndroid Build Coastguard Worker    expected_by = EXPECTED_BY_P3
1378*b7c941bbSAndroid Build Coastguard Worker  elif color_space == 'SRGB':
1379*b7c941bbSAndroid Build Coastguard Worker    # Expected primaries based on Pixel sRGB profile
1380*b7c941bbSAndroid Build Coastguard Worker    expected_rx = EXPECTED_RX_SRGB
1381*b7c941bbSAndroid Build Coastguard Worker    expected_ry = EXPECTED_RY_SRGB
1382*b7c941bbSAndroid Build Coastguard Worker    expected_gx = EXPECTED_GX_SRGB
1383*b7c941bbSAndroid Build Coastguard Worker    expected_gy = EXPECTED_GY_SRGB
1384*b7c941bbSAndroid Build Coastguard Worker    expected_bx = EXPECTED_BX_SRGB
1385*b7c941bbSAndroid Build Coastguard Worker    expected_by = EXPECTED_BY_SRGB
1386*b7c941bbSAndroid Build Coastguard Worker  else:
1387*b7c941bbSAndroid Build Coastguard Worker    # Unsupported color space for comparison
1388*b7c941bbSAndroid Build Coastguard Worker    return False
1389*b7c941bbSAndroid Build Coastguard Worker
1390*b7c941bbSAndroid Build Coastguard Worker  cmp_values = [
1391*b7c941bbSAndroid Build Coastguard Worker      [rx, expected_rx],
1392*b7c941bbSAndroid Build Coastguard Worker      [ry, expected_ry],
1393*b7c941bbSAndroid Build Coastguard Worker      [gx, expected_gx],
1394*b7c941bbSAndroid Build Coastguard Worker      [gy, expected_gy],
1395*b7c941bbSAndroid Build Coastguard Worker      [bx, expected_bx],
1396*b7c941bbSAndroid Build Coastguard Worker      [by, expected_by]
1397*b7c941bbSAndroid Build Coastguard Worker  ]
1398*b7c941bbSAndroid Build Coastguard Worker
1399*b7c941bbSAndroid Build Coastguard Worker  for (actual, expected) in cmp_values:
1400*b7c941bbSAndroid Build Coastguard Worker    if not math.isclose(actual, expected, abs_tol=0.001):
1401*b7c941bbSAndroid Build Coastguard Worker      # Values significantly differ
1402*b7c941bbSAndroid Build Coastguard Worker      return False
1403*b7c941bbSAndroid Build Coastguard Worker
1404*b7c941bbSAndroid Build Coastguard Worker  return True
1405*b7c941bbSAndroid Build Coastguard Worker
1406*b7c941bbSAndroid Build Coastguard Worker
1407*b7c941bbSAndroid Build Coastguard Workerdef area_of_triangle(x1, y1, x2, y2, x3, y3):
1408*b7c941bbSAndroid Build Coastguard Worker  """Calculates the area of a triangle formed by three points.
1409*b7c941bbSAndroid Build Coastguard Worker
1410*b7c941bbSAndroid Build Coastguard Worker  Args:
1411*b7c941bbSAndroid Build Coastguard Worker    x1 (float): The x-coordinate of the first point.
1412*b7c941bbSAndroid Build Coastguard Worker    y1 (float): The y-coordinate of the first point.
1413*b7c941bbSAndroid Build Coastguard Worker    x2 (float): The x-coordinate of the second point.
1414*b7c941bbSAndroid Build Coastguard Worker    y2 (float): The y-coordinate of the second point.
1415*b7c941bbSAndroid Build Coastguard Worker    x3 (float): The x-coordinate of the third point.
1416*b7c941bbSAndroid Build Coastguard Worker    y3 (float): The y-coordinate of the third point.
1417*b7c941bbSAndroid Build Coastguard Worker
1418*b7c941bbSAndroid Build Coastguard Worker  Returns:
1419*b7c941bbSAndroid Build Coastguard Worker    float: The area of the triangle.
1420*b7c941bbSAndroid Build Coastguard Worker  """
1421*b7c941bbSAndroid Build Coastguard Worker  area = abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2.0)
1422*b7c941bbSAndroid Build Coastguard Worker  return area
1423*b7c941bbSAndroid Build Coastguard Worker
1424*b7c941bbSAndroid Build Coastguard Worker
1425*b7c941bbSAndroid Build Coastguard Workerdef point_in_triangle(x1, y1, x2, y2, x3, y3, xp, yp, abs_tol):
1426*b7c941bbSAndroid Build Coastguard Worker  """Checks if the point (xp, yp) is inside the triangle.
1427*b7c941bbSAndroid Build Coastguard Worker
1428*b7c941bbSAndroid Build Coastguard Worker  Args:
1429*b7c941bbSAndroid Build Coastguard Worker    x1 (float): The x-coordinate of the first point.
1430*b7c941bbSAndroid Build Coastguard Worker    y1 (float): The y-coordinate of the first point.
1431*b7c941bbSAndroid Build Coastguard Worker    x2 (float): The x-coordinate of the second point.
1432*b7c941bbSAndroid Build Coastguard Worker    y2 (float): The y-coordinate of the second point.
1433*b7c941bbSAndroid Build Coastguard Worker    x3 (float): The x-coordinate of the third point.
1434*b7c941bbSAndroid Build Coastguard Worker    y3 (float): The y-coordinate of the third point.
1435*b7c941bbSAndroid Build Coastguard Worker    xp (float): The x-coordinate of the point to check.
1436*b7c941bbSAndroid Build Coastguard Worker    yp (float): The y-coordinate of the point to check.
1437*b7c941bbSAndroid Build Coastguard Worker    abs_tol (float): Absolute tolerance amount.
1438*b7c941bbSAndroid Build Coastguard Worker
1439*b7c941bbSAndroid Build Coastguard Worker  Returns:
1440*b7c941bbSAndroid Build Coastguard Worker    bool: True if the point is inside the triangle, False otherwise.
1441*b7c941bbSAndroid Build Coastguard Worker  """
1442*b7c941bbSAndroid Build Coastguard Worker  a = area_of_triangle(x1, y1, x2, y2, x3, y3)
1443*b7c941bbSAndroid Build Coastguard Worker  a1 = area_of_triangle(xp, yp, x2, y2, x3, y3)
1444*b7c941bbSAndroid Build Coastguard Worker  a2 = area_of_triangle(x1, y1, xp, yp, x3, y3)
1445*b7c941bbSAndroid Build Coastguard Worker  a3 = area_of_triangle(x1, y1, x2, y2, xp, yp)
1446*b7c941bbSAndroid Build Coastguard Worker  return math.isclose(a, (a1 + a2 + a3), abs_tol=abs_tol)
1447*b7c941bbSAndroid Build Coastguard Worker
1448*b7c941bbSAndroid Build Coastguard Worker
1449*b7c941bbSAndroid Build Coastguard Workerdef distance(p, q):
1450*b7c941bbSAndroid Build Coastguard Worker  """Returns the Euclidean distance from point p to point q.
1451*b7c941bbSAndroid Build Coastguard Worker
1452*b7c941bbSAndroid Build Coastguard Worker  Args:
1453*b7c941bbSAndroid Build Coastguard Worker    p: an Iterable of numbers
1454*b7c941bbSAndroid Build Coastguard Worker    q: an Iterable of numbers
1455*b7c941bbSAndroid Build Coastguard Worker  """
1456*b7c941bbSAndroid Build Coastguard Worker  return math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))
1457*b7c941bbSAndroid Build Coastguard Worker
1458*b7c941bbSAndroid Build Coastguard Worker
1459*b7c941bbSAndroid Build Coastguard Workerdef srgb_eotf(img):
1460*b7c941bbSAndroid Build Coastguard Worker  """Returns the input sRGB-transferred image with a linear transfer function.
1461*b7c941bbSAndroid Build Coastguard Worker
1462*b7c941bbSAndroid Build Coastguard Worker  Args:
1463*b7c941bbSAndroid Build Coastguard Worker    img: The input image as a numpy array.
1464*b7c941bbSAndroid Build Coastguard Worker
1465*b7c941bbSAndroid Build Coastguard Worker  Returns:
1466*b7c941bbSAndroid Build Coastguard Worker    numpy.array: The same image with a linear transfer.
1467*b7c941bbSAndroid Build Coastguard Worker  """
1468*b7c941bbSAndroid Build Coastguard Worker
1469*b7c941bbSAndroid Build Coastguard Worker  # Source:
1470*b7c941bbSAndroid Build Coastguard Worker  # https://developer.android.com/reference/android/graphics/ColorSpace.Named#DISPLAY_P3
1471*b7c941bbSAndroid Build Coastguard Worker  return numpy.where(
1472*b7c941bbSAndroid Build Coastguard Worker      img < 0.04045,
1473*b7c941bbSAndroid Build Coastguard Worker      img / 12.92,
1474*b7c941bbSAndroid Build Coastguard Worker      numpy.pow((img + 0.055) / 1.055, 2.4)
1475*b7c941bbSAndroid Build Coastguard Worker  )
1476*b7c941bbSAndroid Build Coastguard Worker
1477*b7c941bbSAndroid Build Coastguard Worker
1478*b7c941bbSAndroid Build Coastguard Workerdef ciexyz_to_xy(img):
1479*b7c941bbSAndroid Build Coastguard Worker  """Returns the input CIE XYZ image in the CIE xy colorspace.
1480*b7c941bbSAndroid Build Coastguard Worker
1481*b7c941bbSAndroid Build Coastguard Worker  Args:
1482*b7c941bbSAndroid Build Coastguard Worker    img: The input image as a numpy array
1483*b7c941bbSAndroid Build Coastguard Worker
1484*b7c941bbSAndroid Build Coastguard Worker  Returns:
1485*b7c941bbSAndroid Build Coastguard Worker    numpy.array: The same image in the CIE xy colorspace.
1486*b7c941bbSAndroid Build Coastguard Worker  """
1487*b7c941bbSAndroid Build Coastguard Worker  img_sums = img.sum(axis=2)
1488*b7c941bbSAndroid Build Coastguard Worker  img_sums[img_sums == 0] = 1
1489*b7c941bbSAndroid Build Coastguard Worker  img[:, :, 0] = img[:, :, 0] / img_sums
1490*b7c941bbSAndroid Build Coastguard Worker  img[:, :, 1] = img[:, :, 1] / img_sums
1491*b7c941bbSAndroid Build Coastguard Worker  return img[:, :, :2]
1492*b7c941bbSAndroid Build Coastguard Worker
1493*b7c941bbSAndroid Build Coastguard Worker
1494*b7c941bbSAndroid Build Coastguard Workerdef p3_img_has_wide_gamut(wide_img):
1495*b7c941bbSAndroid Build Coastguard Worker  """Check if a DISPLAY_P3 image contains wide gamut pixels.
1496*b7c941bbSAndroid Build Coastguard Worker
1497*b7c941bbSAndroid Build Coastguard Worker  Given a DISPLAY_P3 image that should have a wider gamut than SRGB, checks all
1498*b7c941bbSAndroid Build Coastguard Worker  pixel values to see if any reside outside the SRGB gamut. This is done by
1499*b7c941bbSAndroid Build Coastguard Worker  converting to CIE xy chromaticities using a Bradford chromatic adaptation for
1500*b7c941bbSAndroid Build Coastguard Worker  consistency with ICC profiles.
1501*b7c941bbSAndroid Build Coastguard Worker
1502*b7c941bbSAndroid Build Coastguard Worker  Args:
1503*b7c941bbSAndroid Build Coastguard Worker    wide_img: The PIL.Image in the DISPLAY_P3 color space.
1504*b7c941bbSAndroid Build Coastguard Worker
1505*b7c941bbSAndroid Build Coastguard Worker  Returns:
1506*b7c941bbSAndroid Build Coastguard Worker    True if the gamut of wide_img is greater than that of SRGB.
1507*b7c941bbSAndroid Build Coastguard Worker    False otherwise.
1508*b7c941bbSAndroid Build Coastguard Worker  """
1509*b7c941bbSAndroid Build Coastguard Worker  w = wide_img.size[0]
1510*b7c941bbSAndroid Build Coastguard Worker  h = wide_img.size[1]
1511*b7c941bbSAndroid Build Coastguard Worker  wide_arr = numpy.array(wide_img)
1512*b7c941bbSAndroid Build Coastguard Worker  linear_arr = srgb_eotf(wide_arr / float(numpy.iinfo(numpy.uint8).max))
1513*b7c941bbSAndroid Build Coastguard Worker
1514*b7c941bbSAndroid Build Coastguard Worker  xyz_arr = numpy.matmul(linear_arr, P3_TO_XYZ)
1515*b7c941bbSAndroid Build Coastguard Worker  xy_arr = ciexyz_to_xy(xyz_arr)
1516*b7c941bbSAndroid Build Coastguard Worker
1517*b7c941bbSAndroid Build Coastguard Worker  for y in range(h):
1518*b7c941bbSAndroid Build Coastguard Worker    for x in range(w):
1519*b7c941bbSAndroid Build Coastguard Worker      # Check if the pixel chromaticity is inside or outside the SRGB gamut.
1520*b7c941bbSAndroid Build Coastguard Worker      # This check is not guaranteed not to emit false positives / negatives,
1521*b7c941bbSAndroid Build Coastguard Worker      # however the probability of either on an arbitrary DISPLAY_P3 camera
1522*b7c941bbSAndroid Build Coastguard Worker      # capture is exceedingly unlikely.
1523*b7c941bbSAndroid Build Coastguard Worker      if not point_in_triangle(x1=EXPECTED_RX_SRGB, y1=EXPECTED_RY_SRGB,
1524*b7c941bbSAndroid Build Coastguard Worker                               x2=EXPECTED_GX_SRGB, y2=EXPECTED_GY_SRGB,
1525*b7c941bbSAndroid Build Coastguard Worker                               x3=EXPECTED_BX_SRGB, y3=EXPECTED_BY_SRGB,
1526*b7c941bbSAndroid Build Coastguard Worker                               xp=xy_arr[y][x][0], yp=xy_arr[y][x][1],
1527*b7c941bbSAndroid Build Coastguard Worker                               abs_tol=COLORSPACE_TRIANGLE_AREA_TOL):
1528*b7c941bbSAndroid Build Coastguard Worker        return True
1529*b7c941bbSAndroid Build Coastguard Worker
1530*b7c941bbSAndroid Build Coastguard Worker  return False
1531*b7c941bbSAndroid Build Coastguard Worker
1532*b7c941bbSAndroid Build Coastguard Worker
1533*b7c941bbSAndroid Build Coastguard Workerdef compute_patch_noise(yuv_img, patch_region):
1534*b7c941bbSAndroid Build Coastguard Worker  """Computes the noise statistics of a flat patch region in an image.
1535*b7c941bbSAndroid Build Coastguard Worker
1536*b7c941bbSAndroid Build Coastguard Worker  For the patch region, the noise statistics are computed for the luma, chroma
1537*b7c941bbSAndroid Build Coastguard Worker  U, and chroma V channels.
1538*b7c941bbSAndroid Build Coastguard Worker
1539*b7c941bbSAndroid Build Coastguard Worker  Args:
1540*b7c941bbSAndroid Build Coastguard Worker    yuv_img: The openCV YUV image to compute noise statistics for.
1541*b7c941bbSAndroid Build Coastguard Worker    patch_region: The (x, y, w, h) region to compute noise statistics for.
1542*b7c941bbSAndroid Build Coastguard Worker  Returns:
1543*b7c941bbSAndroid Build Coastguard Worker    A dictionary of noise statistics with keys luma, chroma_u, chroma_v.
1544*b7c941bbSAndroid Build Coastguard Worker  """
1545*b7c941bbSAndroid Build Coastguard Worker  x, y, w, h = patch_region
1546*b7c941bbSAndroid Build Coastguard Worker  patch = yuv_img[y : y + h, x : x + w]
1547*b7c941bbSAndroid Build Coastguard Worker  return {
1548*b7c941bbSAndroid Build Coastguard Worker      'luma': numpy.std(patch[:, :, 0]),
1549*b7c941bbSAndroid Build Coastguard Worker      'chroma_u': numpy.std(patch[:, :, 1]),
1550*b7c941bbSAndroid Build Coastguard Worker      'chroma_v': numpy.std(patch[:, :, 2]),
1551*b7c941bbSAndroid Build Coastguard Worker  }
1552*b7c941bbSAndroid Build Coastguard Worker
1553*b7c941bbSAndroid Build Coastguard Worker
1554*b7c941bbSAndroid Build Coastguard Workerdef convert_image_coords_to_sensor_coords(
1555*b7c941bbSAndroid Build Coastguard Worker    aa_width, aa_height, coords, img_width, img_height):
1556*b7c941bbSAndroid Build Coastguard Worker  """Transform image coordinates to sensor coordinate system.
1557*b7c941bbSAndroid Build Coastguard Worker
1558*b7c941bbSAndroid Build Coastguard Worker  Calculate the difference between sensor active array and image aspect ratio.
1559*b7c941bbSAndroid Build Coastguard Worker  Taking the difference into account, figure out if the width or height has been
1560*b7c941bbSAndroid Build Coastguard Worker  cropped. Using this information, transform the image coordinates to sensor
1561*b7c941bbSAndroid Build Coastguard Worker  coordinates.
1562*b7c941bbSAndroid Build Coastguard Worker
1563*b7c941bbSAndroid Build Coastguard Worker  Args:
1564*b7c941bbSAndroid Build Coastguard Worker    aa_width: int; active array width.
1565*b7c941bbSAndroid Build Coastguard Worker    aa_height: int; active array height.
1566*b7c941bbSAndroid Build Coastguard Worker    coords: coordinates; a pair of (x, y) coordinates from image.
1567*b7c941bbSAndroid Build Coastguard Worker    img_width: int; width of image.
1568*b7c941bbSAndroid Build Coastguard Worker    img_height: int; height of image.
1569*b7c941bbSAndroid Build Coastguard Worker  Returns:
1570*b7c941bbSAndroid Build Coastguard Worker    sensor_coords: coordinates; corresponding coordinates on
1571*b7c941bbSAndroid Build Coastguard Worker      sensor coordinate system.
1572*b7c941bbSAndroid Build Coastguard Worker  """
1573*b7c941bbSAndroid Build Coastguard Worker  # TODO: b/330382627 - find out if distortion correction is ON/OFF
1574*b7c941bbSAndroid Build Coastguard Worker  aa_aspect_ratio = aa_width / aa_height
1575*b7c941bbSAndroid Build Coastguard Worker  image_aspect_ratio = img_width / img_height
1576*b7c941bbSAndroid Build Coastguard Worker  if aa_aspect_ratio >= image_aspect_ratio:
1577*b7c941bbSAndroid Build Coastguard Worker    # If aa aspect ratio is greater than image aspect ratio, then
1578*b7c941bbSAndroid Build Coastguard Worker    # sensor width is being cropped
1579*b7c941bbSAndroid Build Coastguard Worker    aspect_ratio_multiplication_factor = aa_height / img_height
1580*b7c941bbSAndroid Build Coastguard Worker    crop_width = img_width * aspect_ratio_multiplication_factor
1581*b7c941bbSAndroid Build Coastguard Worker    buffer = (aa_width - crop_width) / 2
1582*b7c941bbSAndroid Build Coastguard Worker    sensor_coords = (coords[0] * aspect_ratio_multiplication_factor + buffer,
1583*b7c941bbSAndroid Build Coastguard Worker                     coords[1] * aspect_ratio_multiplication_factor)
1584*b7c941bbSAndroid Build Coastguard Worker  else:
1585*b7c941bbSAndroid Build Coastguard Worker    # If aa aspect ratio is less than image aspect ratio, then
1586*b7c941bbSAndroid Build Coastguard Worker    # sensor height is being cropped
1587*b7c941bbSAndroid Build Coastguard Worker    aspect_ratio_multiplication_factor = aa_width / img_width
1588*b7c941bbSAndroid Build Coastguard Worker    crop_height = img_height * aspect_ratio_multiplication_factor
1589*b7c941bbSAndroid Build Coastguard Worker    buffer = (aa_height - crop_height) / 2
1590*b7c941bbSAndroid Build Coastguard Worker    sensor_coords = (coords[0] * aspect_ratio_multiplication_factor,
1591*b7c941bbSAndroid Build Coastguard Worker                     coords[1] * aspect_ratio_multiplication_factor + buffer)
1592*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Sensor coordinates: %s', sensor_coords)
1593*b7c941bbSAndroid Build Coastguard Worker  return sensor_coords
1594*b7c941bbSAndroid Build Coastguard Worker
1595*b7c941bbSAndroid Build Coastguard Worker
1596*b7c941bbSAndroid Build Coastguard Workerdef convert_sensor_coords_to_image_coords(
1597*b7c941bbSAndroid Build Coastguard Worker    aa_width, aa_height, coords, img_width, img_height):
1598*b7c941bbSAndroid Build Coastguard Worker  """Transform sensor coordinates to image coordinate system.
1599*b7c941bbSAndroid Build Coastguard Worker
1600*b7c941bbSAndroid Build Coastguard Worker  Calculate the difference between sensor active array and image aspect ratio.
1601*b7c941bbSAndroid Build Coastguard Worker  Taking the difference into account, figure out if the width or height has been
1602*b7c941bbSAndroid Build Coastguard Worker  cropped. Using this information, transform the sensor coordinates to image
1603*b7c941bbSAndroid Build Coastguard Worker  coordinates.
1604*b7c941bbSAndroid Build Coastguard Worker
1605*b7c941bbSAndroid Build Coastguard Worker  Args:
1606*b7c941bbSAndroid Build Coastguard Worker    aa_width: int; active array width.
1607*b7c941bbSAndroid Build Coastguard Worker    aa_height: int; active array height.
1608*b7c941bbSAndroid Build Coastguard Worker    coords: coordinates; a pair of (x, y) coordinates from sensor.
1609*b7c941bbSAndroid Build Coastguard Worker    img_width: int; width of image.
1610*b7c941bbSAndroid Build Coastguard Worker    img_height: int; height of image.
1611*b7c941bbSAndroid Build Coastguard Worker  Returns:
1612*b7c941bbSAndroid Build Coastguard Worker    image_coords: coordinates; corresponding coordinates on
1613*b7c941bbSAndroid Build Coastguard Worker      image coordinate system.
1614*b7c941bbSAndroid Build Coastguard Worker  """
1615*b7c941bbSAndroid Build Coastguard Worker  aa_aspect_ratio = aa_width / aa_height
1616*b7c941bbSAndroid Build Coastguard Worker  image_aspect_ratio = img_width / img_height
1617*b7c941bbSAndroid Build Coastguard Worker  if aa_aspect_ratio >= image_aspect_ratio:
1618*b7c941bbSAndroid Build Coastguard Worker    # If aa aspect ratio is greater than image aspect ratio, then
1619*b7c941bbSAndroid Build Coastguard Worker    # sensor width is being cropped
1620*b7c941bbSAndroid Build Coastguard Worker    aspect_ratio_multiplication_factor = aa_height / img_height
1621*b7c941bbSAndroid Build Coastguard Worker    crop_width = img_width * aspect_ratio_multiplication_factor
1622*b7c941bbSAndroid Build Coastguard Worker    buffer = (aa_width - crop_width) / 2
1623*b7c941bbSAndroid Build Coastguard Worker    image_coords = (
1624*b7c941bbSAndroid Build Coastguard Worker        (coords[0] - buffer) / aspect_ratio_multiplication_factor,
1625*b7c941bbSAndroid Build Coastguard Worker        coords[1] / aspect_ratio_multiplication_factor)
1626*b7c941bbSAndroid Build Coastguard Worker  else:
1627*b7c941bbSAndroid Build Coastguard Worker    # If aa aspect ratio is less than image aspect ratio, then
1628*b7c941bbSAndroid Build Coastguard Worker    # sensor height is being cropped
1629*b7c941bbSAndroid Build Coastguard Worker    aspect_ratio_multiplication_factor = aa_width / img_width
1630*b7c941bbSAndroid Build Coastguard Worker    crop_height = img_height * aspect_ratio_multiplication_factor
1631*b7c941bbSAndroid Build Coastguard Worker    buffer = (aa_height - crop_height) / 2
1632*b7c941bbSAndroid Build Coastguard Worker    image_coords = (
1633*b7c941bbSAndroid Build Coastguard Worker        coords[0] / aspect_ratio_multiplication_factor,
1634*b7c941bbSAndroid Build Coastguard Worker        (coords[1] - buffer) / aspect_ratio_multiplication_factor)
1635*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Image coordinates: %s', image_coords)
1636*b7c941bbSAndroid Build Coastguard Worker  return image_coords
1637