xref: /aosp_15_r20/cts/apps/CameraITS/utils/low_light_utils.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1*b7c941bbSAndroid Build Coastguard Worker# Copyright 2024 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"""Utility functions for low light camera tests."""
15*b7c941bbSAndroid Build Coastguard Worker
16*b7c941bbSAndroid Build Coastguard Workerimport logging
17*b7c941bbSAndroid Build Coastguard Workerimport os.path
18*b7c941bbSAndroid Build Coastguard Worker
19*b7c941bbSAndroid Build Coastguard Workerimport camera_properties_utils
20*b7c941bbSAndroid Build Coastguard Workerimport capture_request_utils
21*b7c941bbSAndroid Build Coastguard Workerimport cv2
22*b7c941bbSAndroid Build Coastguard Workerimport image_processing_utils
23*b7c941bbSAndroid Build Coastguard Workerimport matplotlib.pyplot as plt
24*b7c941bbSAndroid Build Coastguard Workerimport numpy as np
25*b7c941bbSAndroid Build Coastguard Workerimport opencv_processing_utils
26*b7c941bbSAndroid Build Coastguard Worker
27*b7c941bbSAndroid Build Coastguard Worker_LOW_LIGHT_BOOST_AVG_DELTA_LUMINANCE_THRESH = 18
28*b7c941bbSAndroid Build Coastguard Worker_LOW_LIGHT_BOOST_AVG_LUMINANCE_THRESH = 90
29*b7c941bbSAndroid Build Coastguard Worker_BOUNDING_BOX_COLOR = (0, 255, 0)
30*b7c941bbSAndroid Build Coastguard Worker_BOX_MIN_SIZE_RATIO = 0.08  # 8% of the cropped image width
31*b7c941bbSAndroid Build Coastguard Worker_BOX_MAX_SIZE_RATIO = 0.5  # 50% of the cropped image width
32*b7c941bbSAndroid Build Coastguard Worker_BOX_PADDING_RATIO = 0.2
33*b7c941bbSAndroid Build Coastguard Worker_CROP_PADDING = 10
34*b7c941bbSAndroid Build Coastguard Worker_EXPECTED_NUM_OF_BOXES = 20  # The captured image must result in 20 detected
35*b7c941bbSAndroid Build Coastguard Worker                             # boxes since the test scene has 20 boxes
36*b7c941bbSAndroid Build Coastguard Worker_KEY_BOTTOM_LEFT = 'bottom_left'
37*b7c941bbSAndroid Build Coastguard Worker_KEY_BOTTOM_RIGHT = 'bottom_right'
38*b7c941bbSAndroid Build Coastguard Worker_KEY_TOP_LEFT = 'top_left'
39*b7c941bbSAndroid Build Coastguard Worker_KEY_TOP_RIGHT = 'top_right'
40*b7c941bbSAndroid Build Coastguard Worker_MAX_ASPECT_RATIO = 1.2
41*b7c941bbSAndroid Build Coastguard Worker_MIN_ASPECT_RATIO = 0.8
42*b7c941bbSAndroid Build Coastguard Worker_RED_BGR_COLOR = (0, 0, 255)
43*b7c941bbSAndroid Build Coastguard Worker_NUM_CLUSTERS = 8
44*b7c941bbSAndroid Build Coastguard Worker_K_MEANS_ITERATIONS = 10
45*b7c941bbSAndroid Build Coastguard Worker_K_MEANS_EPSILON = 0.5
46*b7c941bbSAndroid Build Coastguard Worker_TEXT_COLOR = (255, 255, 255)
47*b7c941bbSAndroid Build Coastguard Worker_FIG_SIZE = (10, 6)
48*b7c941bbSAndroid Build Coastguard Worker
49*b7c941bbSAndroid Build Coastguard Worker# Allowed tablets for low light scenes
50*b7c941bbSAndroid Build Coastguard Worker# List entries must be entered in lowercase
51*b7c941bbSAndroid Build Coastguard WorkerTABLET_LOW_LIGHT_SCENES_ALLOWLIST = (
52*b7c941bbSAndroid Build Coastguard Worker    'hwcmr09',  # Huawei MediaPad M5
53*b7c941bbSAndroid Build Coastguard Worker    'gta8wifi',  # Samsung Galaxy Tab A8
54*b7c941bbSAndroid Build Coastguard Worker    'gta8',  # Samsung Galaxy Tab A8 LTE
55*b7c941bbSAndroid Build Coastguard Worker    'gta9pwifi',  # Samsung Galaxy Tab A9+
56*b7c941bbSAndroid Build Coastguard Worker    'gta9p',  # Samsung Galaxy Tab A9+ 5G
57*b7c941bbSAndroid Build Coastguard Worker    'nabu',  # Xiaomi Pad 5
58*b7c941bbSAndroid Build Coastguard Worker    'nabu_tw',  # Xiaomi Pad 5
59*b7c941bbSAndroid Build Coastguard Worker    'xun',  # Xiaomi Redmi Pad SE
60*b7c941bbSAndroid Build Coastguard Worker)
61*b7c941bbSAndroid Build Coastguard Worker
62*b7c941bbSAndroid Build Coastguard Worker# Tablet brightness mapping strings for (rear, front) facing camera tests
63*b7c941bbSAndroid Build Coastguard Worker# List entries must be entered in lowercase
64*b7c941bbSAndroid Build Coastguard WorkerTABLET_BRIGHTNESS = {
65*b7c941bbSAndroid Build Coastguard Worker    'hwcmr09': ('4', '8'),  # Huawei MediaPad M5
66*b7c941bbSAndroid Build Coastguard Worker    'gta8wifi': ('6', '12'),  # Samsung Galaxy Tab A8
67*b7c941bbSAndroid Build Coastguard Worker    'gta8': ('6', '12'),  # Samsung Galaxy Tab A8 LTE
68*b7c941bbSAndroid Build Coastguard Worker    'gta9pwifi': ('6', '12'),  # Samsung Galaxy Tab A9+
69*b7c941bbSAndroid Build Coastguard Worker    'gta9p': ('6', '12'),  # Samsung Galaxy Tab A9+ 5G
70*b7c941bbSAndroid Build Coastguard Worker    'nabu': ('8', '14'),  # Xiaomi Pad 5
71*b7c941bbSAndroid Build Coastguard Worker    'nabu_tw': ('8', '14'),  # Xiaomi Pad 5
72*b7c941bbSAndroid Build Coastguard Worker    'xun': ('6', '12'),  # Xiaomi Redmi Pad SE
73*b7c941bbSAndroid Build Coastguard Worker}
74*b7c941bbSAndroid Build Coastguard Worker
75*b7c941bbSAndroid Build Coastguard Worker
76*b7c941bbSAndroid Build Coastguard Workerdef get_metering_region(cam, file_stem):
77*b7c941bbSAndroid Build Coastguard Worker  """Get the metering region for the given image.
78*b7c941bbSAndroid Build Coastguard Worker
79*b7c941bbSAndroid Build Coastguard Worker  Detects the chart in the preview image and returns the coordinates of the
80*b7c941bbSAndroid Build Coastguard Worker  chart in the active array.
81*b7c941bbSAndroid Build Coastguard Worker
82*b7c941bbSAndroid Build Coastguard Worker  Args:
83*b7c941bbSAndroid Build Coastguard Worker    cam: ItsSession object to send commands.
84*b7c941bbSAndroid Build Coastguard Worker    file_stem: File prefix for captured images.
85*b7c941bbSAndroid Build Coastguard Worker  Returns:
86*b7c941bbSAndroid Build Coastguard Worker    The metering region sensor coordinates in the active array or None if the
87*b7c941bbSAndroid Build Coastguard Worker    test chart was not detected.
88*b7c941bbSAndroid Build Coastguard Worker  """
89*b7c941bbSAndroid Build Coastguard Worker  req = capture_request_utils.auto_capture_request()
90*b7c941bbSAndroid Build Coastguard Worker  cap = cam.do_capture(req, cam.CAP_YUV)
91*b7c941bbSAndroid Build Coastguard Worker  img = image_processing_utils.convert_capture_to_rgb_image(cap)
92*b7c941bbSAndroid Build Coastguard Worker  region_detection_file = f'{file_stem}_region_detection.jpg'
93*b7c941bbSAndroid Build Coastguard Worker  image_processing_utils.write_image(img, region_detection_file)
94*b7c941bbSAndroid Build Coastguard Worker  img = cv2.imread(region_detection_file)
95*b7c941bbSAndroid Build Coastguard Worker
96*b7c941bbSAndroid Build Coastguard Worker  coords = _find_chart_bounding_region(img)
97*b7c941bbSAndroid Build Coastguard Worker  if coords is None:
98*b7c941bbSAndroid Build Coastguard Worker    return None
99*b7c941bbSAndroid Build Coastguard Worker
100*b7c941bbSAndroid Build Coastguard Worker  # Convert image coordinates to sensor coordinates for metering rectangle
101*b7c941bbSAndroid Build Coastguard Worker  img_w = img.shape[1]
102*b7c941bbSAndroid Build Coastguard Worker  img_h = img.shape[0]
103*b7c941bbSAndroid Build Coastguard Worker  props = cam.get_camera_properties()
104*b7c941bbSAndroid Build Coastguard Worker  aa = props['android.sensor.info.activeArraySize']
105*b7c941bbSAndroid Build Coastguard Worker  aa_width, aa_height = aa['right'] - aa['left'], aa['bottom'] - aa['top']
106*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Active array size: %s', aa)
107*b7c941bbSAndroid Build Coastguard Worker  coords_tl = (coords[0], coords[1])
108*b7c941bbSAndroid Build Coastguard Worker  coords_br = (coords[0] + coords[2], coords[1] + coords[3])
109*b7c941bbSAndroid Build Coastguard Worker  s_coords_tl = image_processing_utils.convert_image_coords_to_sensor_coords(
110*b7c941bbSAndroid Build Coastguard Worker      aa_width, aa_height, coords_tl, img_w, img_h)
111*b7c941bbSAndroid Build Coastguard Worker  s_coords_br = image_processing_utils.convert_image_coords_to_sensor_coords(
112*b7c941bbSAndroid Build Coastguard Worker      aa_width, aa_height, coords_br, img_w, img_h)
113*b7c941bbSAndroid Build Coastguard Worker  sensor_coords = (s_coords_tl[0], s_coords_tl[1], s_coords_br[0],
114*b7c941bbSAndroid Build Coastguard Worker                   s_coords_br[1])
115*b7c941bbSAndroid Build Coastguard Worker
116*b7c941bbSAndroid Build Coastguard Worker  # If testing front camera, mirror coordinates either left/right or up/down
117*b7c941bbSAndroid Build Coastguard Worker  # Preview are flipped on device's natural orientation
118*b7c941bbSAndroid Build Coastguard Worker  # For sensor orientation 90 or 270, it is up or down
119*b7c941bbSAndroid Build Coastguard Worker  # For sensor orientation 0 or 180, it is left or right
120*b7c941bbSAndroid Build Coastguard Worker  if (props['android.lens.facing'] ==
121*b7c941bbSAndroid Build Coastguard Worker      camera_properties_utils.LENS_FACING['FRONT']):
122*b7c941bbSAndroid Build Coastguard Worker    if props['android.sensor.orientation'] in (90, 270):
123*b7c941bbSAndroid Build Coastguard Worker      tl_coordinates = (sensor_coords[0], aa_height - sensor_coords[3])
124*b7c941bbSAndroid Build Coastguard Worker      br_coordinates = (sensor_coords[2], aa_height - sensor_coords[1])
125*b7c941bbSAndroid Build Coastguard Worker      logging.debug('Found sensor orientation %d, flipping up down',
126*b7c941bbSAndroid Build Coastguard Worker                    props['android.sensor.orientation'])
127*b7c941bbSAndroid Build Coastguard Worker    else:
128*b7c941bbSAndroid Build Coastguard Worker      tl_coordinates = (aa_width - sensor_coords[2], sensor_coords[1])
129*b7c941bbSAndroid Build Coastguard Worker      br_coordinates = (aa_width - sensor_coords[0], sensor_coords[3])
130*b7c941bbSAndroid Build Coastguard Worker      logging.debug('Found sensor orientation %d, flipping left right',
131*b7c941bbSAndroid Build Coastguard Worker                    props['android.sensor.orientation'])
132*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Mirrored top-left coordinates: %s', tl_coordinates)
133*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Mirrored bottom-right coordinates: %s', br_coordinates)
134*b7c941bbSAndroid Build Coastguard Worker  else:
135*b7c941bbSAndroid Build Coastguard Worker    tl_coordinates = (sensor_coords[0], sensor_coords[1])
136*b7c941bbSAndroid Build Coastguard Worker    br_coordinates = (sensor_coords[2], sensor_coords[3])
137*b7c941bbSAndroid Build Coastguard Worker
138*b7c941bbSAndroid Build Coastguard Worker  tl_x = int(tl_coordinates[0])
139*b7c941bbSAndroid Build Coastguard Worker  tl_y = int(tl_coordinates[1])
140*b7c941bbSAndroid Build Coastguard Worker  br_x = int(br_coordinates[0])
141*b7c941bbSAndroid Build Coastguard Worker  br_y = int(br_coordinates[1])
142*b7c941bbSAndroid Build Coastguard Worker  rect_w = br_x - tl_x
143*b7c941bbSAndroid Build Coastguard Worker  rect_h = br_y - tl_y
144*b7c941bbSAndroid Build Coastguard Worker  return {'x': tl_x,
145*b7c941bbSAndroid Build Coastguard Worker          'y': tl_y,
146*b7c941bbSAndroid Build Coastguard Worker          'width': rect_w,
147*b7c941bbSAndroid Build Coastguard Worker          'height': rect_h,
148*b7c941bbSAndroid Build Coastguard Worker          'weight': opencv_processing_utils.AE_AWB_METER_WEIGHT}
149*b7c941bbSAndroid Build Coastguard Worker
150*b7c941bbSAndroid Build Coastguard Worker
151*b7c941bbSAndroid Build Coastguard Workerdef _find_chart_bounding_region(img):
152*b7c941bbSAndroid Build Coastguard Worker  """Finds the bounding region of the chart.
153*b7c941bbSAndroid Build Coastguard Worker
154*b7c941bbSAndroid Build Coastguard Worker  Args:
155*b7c941bbSAndroid Build Coastguard Worker    img: numpy array; captured image from scene_low_light.
156*b7c941bbSAndroid Build Coastguard Worker  Returns:
157*b7c941bbSAndroid Build Coastguard Worker    The coordinates of the bounding region relative to the input image. This
158*b7c941bbSAndroid Build Coastguard Worker    is returned as (left, top, width, height) or None if the test chart was
159*b7c941bbSAndroid Build Coastguard Worker    not detected.
160*b7c941bbSAndroid Build Coastguard Worker  """
161*b7c941bbSAndroid Build Coastguard Worker  # To apply k-means clustering, we need to convert the image in to an array
162*b7c941bbSAndroid Build Coastguard Worker  # where each row represents a pixel in the image, and each column is a feature
163*b7c941bbSAndroid Build Coastguard Worker  # In this case, the feature represents the RGB channels of the pixel
164*b7c941bbSAndroid Build Coastguard Worker  data = img.reshape((-1, 3))
165*b7c941bbSAndroid Build Coastguard Worker  data = np.float32(data)
166*b7c941bbSAndroid Build Coastguard Worker
167*b7c941bbSAndroid Build Coastguard Worker  k_means_criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
168*b7c941bbSAndroid Build Coastguard Worker                      _K_MEANS_ITERATIONS, _K_MEANS_EPSILON)
169*b7c941bbSAndroid Build Coastguard Worker  _, labels, centers = cv2.kmeans(data, _NUM_CLUSTERS, None, k_means_criteria,
170*b7c941bbSAndroid Build Coastguard Worker                                  _K_MEANS_ITERATIONS,
171*b7c941bbSAndroid Build Coastguard Worker                                  cv2.KMEANS_RANDOM_CENTERS)
172*b7c941bbSAndroid Build Coastguard Worker  # Find the cluster closest to red
173*b7c941bbSAndroid Build Coastguard Worker  min_dist = float('inf')
174*b7c941bbSAndroid Build Coastguard Worker  closest_cluster_index = -1
175*b7c941bbSAndroid Build Coastguard Worker  for index, center in enumerate(centers):
176*b7c941bbSAndroid Build Coastguard Worker    dist = np.linalg.norm(center - np.array(_RED_BGR_COLOR))
177*b7c941bbSAndroid Build Coastguard Worker    if dist < min_dist:
178*b7c941bbSAndroid Build Coastguard Worker      min_dist = dist
179*b7c941bbSAndroid Build Coastguard Worker      closest_cluster_index = index
180*b7c941bbSAndroid Build Coastguard Worker
181*b7c941bbSAndroid Build Coastguard Worker  target_label = closest_cluster_index
182*b7c941bbSAndroid Build Coastguard Worker
183*b7c941bbSAndroid Build Coastguard Worker  # create a mask using the data associated with the cluster closest to red
184*b7c941bbSAndroid Build Coastguard Worker  mask = labels.flatten() == target_label
185*b7c941bbSAndroid Build Coastguard Worker  mask = mask.reshape((img.shape[0], img.shape[1]))
186*b7c941bbSAndroid Build Coastguard Worker  mask = mask.astype(np.uint8)
187*b7c941bbSAndroid Build Coastguard Worker
188*b7c941bbSAndroid Build Coastguard Worker  contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
189*b7c941bbSAndroid Build Coastguard Worker                                 cv2.CHAIN_APPROX_SIMPLE)
190*b7c941bbSAndroid Build Coastguard Worker
191*b7c941bbSAndroid Build Coastguard Worker  max_area = 20
192*b7c941bbSAndroid Build Coastguard Worker  max_box = None
193*b7c941bbSAndroid Build Coastguard Worker
194*b7c941bbSAndroid Build Coastguard Worker  # Find the largest box that is closest to square
195*b7c941bbSAndroid Build Coastguard Worker  for c in contours:
196*b7c941bbSAndroid Build Coastguard Worker    x, y, w, h = cv2.boundingRect(c)
197*b7c941bbSAndroid Build Coastguard Worker    aspect_ratio = w / h
198*b7c941bbSAndroid Build Coastguard Worker    if _MIN_ASPECT_RATIO < aspect_ratio < _MAX_ASPECT_RATIO:
199*b7c941bbSAndroid Build Coastguard Worker      area = w * h
200*b7c941bbSAndroid Build Coastguard Worker      if area > max_area:
201*b7c941bbSAndroid Build Coastguard Worker        max_area = area
202*b7c941bbSAndroid Build Coastguard Worker        max_box = (x, y, w, h)
203*b7c941bbSAndroid Build Coastguard Worker
204*b7c941bbSAndroid Build Coastguard Worker  return max_box
205*b7c941bbSAndroid Build Coastguard Worker
206*b7c941bbSAndroid Build Coastguard Worker
207*b7c941bbSAndroid Build Coastguard Workerdef _crop(img):
208*b7c941bbSAndroid Build Coastguard Worker  """Crops the captured image according to the red square outline.
209*b7c941bbSAndroid Build Coastguard Worker
210*b7c941bbSAndroid Build Coastguard Worker  Args:
211*b7c941bbSAndroid Build Coastguard Worker    img: numpy array; captured image from scene_low_light.
212*b7c941bbSAndroid Build Coastguard Worker  Returns:
213*b7c941bbSAndroid Build Coastguard Worker    numpy array of the cropped image or the original image if the crop region
214*b7c941bbSAndroid Build Coastguard Worker    isn't found.
215*b7c941bbSAndroid Build Coastguard Worker  """
216*b7c941bbSAndroid Build Coastguard Worker  max_box = _find_chart_bounding_region(img)
217*b7c941bbSAndroid Build Coastguard Worker
218*b7c941bbSAndroid Build Coastguard Worker  # If the box is found then return the cropped image
219*b7c941bbSAndroid Build Coastguard Worker  # otherwise the original image is returned
220*b7c941bbSAndroid Build Coastguard Worker  if max_box:
221*b7c941bbSAndroid Build Coastguard Worker    x, y, w, h = max_box
222*b7c941bbSAndroid Build Coastguard Worker    cropped_img = img[
223*b7c941bbSAndroid Build Coastguard Worker        y+_CROP_PADDING:y+h-_CROP_PADDING,
224*b7c941bbSAndroid Build Coastguard Worker        x+_CROP_PADDING:x+w-_CROP_PADDING
225*b7c941bbSAndroid Build Coastguard Worker    ]
226*b7c941bbSAndroid Build Coastguard Worker    return cropped_img
227*b7c941bbSAndroid Build Coastguard Worker
228*b7c941bbSAndroid Build Coastguard Worker  return img
229*b7c941bbSAndroid Build Coastguard Worker
230*b7c941bbSAndroid Build Coastguard Worker
231*b7c941bbSAndroid Build Coastguard Workerdef _find_boxes(image):
232*b7c941bbSAndroid Build Coastguard Worker  """Finds boxes in the captured image for computing luminance.
233*b7c941bbSAndroid Build Coastguard Worker
234*b7c941bbSAndroid Build Coastguard Worker  The captured image should be of scene_low_light.png. The boxes are detected
235*b7c941bbSAndroid Build Coastguard Worker  by finding the contours by applying a threshold followed erosion.
236*b7c941bbSAndroid Build Coastguard Worker
237*b7c941bbSAndroid Build Coastguard Worker  Args:
238*b7c941bbSAndroid Build Coastguard Worker    image: numpy array; the captured image.
239*b7c941bbSAndroid Build Coastguard Worker  Returns:
240*b7c941bbSAndroid Build Coastguard Worker    array; an array of boxes, where each box is (x, y, w, h).
241*b7c941bbSAndroid Build Coastguard Worker  """
242*b7c941bbSAndroid Build Coastguard Worker  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
243*b7c941bbSAndroid Build Coastguard Worker  blur = cv2.GaussianBlur(gray, (3, 3), 0)
244*b7c941bbSAndroid Build Coastguard Worker
245*b7c941bbSAndroid Build Coastguard Worker  thresh = cv2.adaptiveThreshold(
246*b7c941bbSAndroid Build Coastguard Worker      blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 31, -5)
247*b7c941bbSAndroid Build Coastguard Worker
248*b7c941bbSAndroid Build Coastguard Worker  kernel = np.ones((3, 3), np.uint8)
249*b7c941bbSAndroid Build Coastguard Worker  eroded = cv2.erode(thresh, kernel, iterations=1)
250*b7c941bbSAndroid Build Coastguard Worker
251*b7c941bbSAndroid Build Coastguard Worker  contours, _ = cv2.findContours(eroded, cv2.RETR_EXTERNAL,
252*b7c941bbSAndroid Build Coastguard Worker                                 cv2.CHAIN_APPROX_SIMPLE)
253*b7c941bbSAndroid Build Coastguard Worker  boxes = []
254*b7c941bbSAndroid Build Coastguard Worker
255*b7c941bbSAndroid Build Coastguard Worker  # Filter out boxes that are too small or too large
256*b7c941bbSAndroid Build Coastguard Worker  # and boxes that are not square
257*b7c941bbSAndroid Build Coastguard Worker  img_hw_size_max = max(image.shape[0], image.shape[1])
258*b7c941bbSAndroid Build Coastguard Worker  box_min_size = int(round(img_hw_size_max * _BOX_MIN_SIZE_RATIO, 0))
259*b7c941bbSAndroid Build Coastguard Worker  if box_min_size == 0:
260*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('Minimum box size calculated was 0. Check cropped '
261*b7c941bbSAndroid Build Coastguard Worker                         'image size.')
262*b7c941bbSAndroid Build Coastguard Worker  box_max_size = int(img_hw_size_max * _BOX_MAX_SIZE_RATIO)
263*b7c941bbSAndroid Build Coastguard Worker  for c in contours:
264*b7c941bbSAndroid Build Coastguard Worker    x, y, w, h = cv2.boundingRect(c)
265*b7c941bbSAndroid Build Coastguard Worker    aspect_ratio = w / h
266*b7c941bbSAndroid Build Coastguard Worker    if (w > box_min_size and h > box_min_size and
267*b7c941bbSAndroid Build Coastguard Worker        w < box_max_size and h < box_max_size and
268*b7c941bbSAndroid Build Coastguard Worker        _MIN_ASPECT_RATIO < aspect_ratio < _MAX_ASPECT_RATIO):
269*b7c941bbSAndroid Build Coastguard Worker      boxes.append((x, y, w, h))
270*b7c941bbSAndroid Build Coastguard Worker  return boxes
271*b7c941bbSAndroid Build Coastguard Worker
272*b7c941bbSAndroid Build Coastguard Worker
273*b7c941bbSAndroid Build Coastguard Workerdef _correct_image_rotation(img, regions):
274*b7c941bbSAndroid Build Coastguard Worker  """Corrects the captured image orientation.
275*b7c941bbSAndroid Build Coastguard Worker
276*b7c941bbSAndroid Build Coastguard Worker  The captured image should be of scene_low_light.png. The darkest square
277*b7c941bbSAndroid Build Coastguard Worker  must appear in the bottom right and the brightest square must appear in
278*b7c941bbSAndroid Build Coastguard Worker  the bottom left. This is necessary in order to traverse the hilbert
279*b7c941bbSAndroid Build Coastguard Worker  ordered squares to return a darkest to brightest ordering.
280*b7c941bbSAndroid Build Coastguard Worker
281*b7c941bbSAndroid Build Coastguard Worker  Args:
282*b7c941bbSAndroid Build Coastguard Worker    img: numpy array; the original image captured.
283*b7c941bbSAndroid Build Coastguard Worker    regions: the tuple of (box, luminance) computed for each square
284*b7c941bbSAndroid Build Coastguard Worker      in the image.
285*b7c941bbSAndroid Build Coastguard Worker  Returns:
286*b7c941bbSAndroid Build Coastguard Worker    numpy array; image in the corrected orientation.
287*b7c941bbSAndroid Build Coastguard Worker  """
288*b7c941bbSAndroid Build Coastguard Worker  corner_brightness = {
289*b7c941bbSAndroid Build Coastguard Worker      _KEY_TOP_LEFT: regions[2][1],
290*b7c941bbSAndroid Build Coastguard Worker      _KEY_BOTTOM_LEFT: regions[5][1],
291*b7c941bbSAndroid Build Coastguard Worker      _KEY_TOP_RIGHT: regions[14][1],
292*b7c941bbSAndroid Build Coastguard Worker      _KEY_BOTTOM_RIGHT: regions[17][1],
293*b7c941bbSAndroid Build Coastguard Worker  }
294*b7c941bbSAndroid Build Coastguard Worker
295*b7c941bbSAndroid Build Coastguard Worker  darkest_corner = ('', float('inf'))
296*b7c941bbSAndroid Build Coastguard Worker  brightest_corner = ('', float('-inf'))
297*b7c941bbSAndroid Build Coastguard Worker
298*b7c941bbSAndroid Build Coastguard Worker  for corner, luminance in corner_brightness.items():
299*b7c941bbSAndroid Build Coastguard Worker    if luminance < darkest_corner[1]:
300*b7c941bbSAndroid Build Coastguard Worker      darkest_corner = (corner, luminance)
301*b7c941bbSAndroid Build Coastguard Worker    if luminance > brightest_corner[1]:
302*b7c941bbSAndroid Build Coastguard Worker      brightest_corner = (corner, luminance)
303*b7c941bbSAndroid Build Coastguard Worker
304*b7c941bbSAndroid Build Coastguard Worker  if darkest_corner == brightest_corner:
305*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('The captured image failed to detect the location '
306*b7c941bbSAndroid Build Coastguard Worker                         'of the darkest and brightest squares.')
307*b7c941bbSAndroid Build Coastguard Worker
308*b7c941bbSAndroid Build Coastguard Worker  if darkest_corner[0] == _KEY_TOP_LEFT:
309*b7c941bbSAndroid Build Coastguard Worker    if brightest_corner[0] == _KEY_BOTTOM_LEFT:
310*b7c941bbSAndroid Build Coastguard Worker      # rotate 90 CW and then flip vertically
311*b7c941bbSAndroid Build Coastguard Worker      img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
312*b7c941bbSAndroid Build Coastguard Worker      img = cv2.flip(img, 0)
313*b7c941bbSAndroid Build Coastguard Worker    elif brightest_corner[0] == _KEY_TOP_RIGHT:
314*b7c941bbSAndroid Build Coastguard Worker      # flip both vertically and horizontally
315*b7c941bbSAndroid Build Coastguard Worker      img = cv2.flip(img, -1)
316*b7c941bbSAndroid Build Coastguard Worker    else:
317*b7c941bbSAndroid Build Coastguard Worker      raise AssertionError('The captured image failed to detect the location '
318*b7c941bbSAndroid Build Coastguard Worker                           'of the brightest square.')
319*b7c941bbSAndroid Build Coastguard Worker  elif darkest_corner[0] == _KEY_BOTTOM_LEFT:
320*b7c941bbSAndroid Build Coastguard Worker    if brightest_corner[0] == _KEY_TOP_LEFT:
321*b7c941bbSAndroid Build Coastguard Worker      # rotate 90 CCW
322*b7c941bbSAndroid Build Coastguard Worker      img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
323*b7c941bbSAndroid Build Coastguard Worker    elif brightest_corner[0] == _KEY_BOTTOM_RIGHT:
324*b7c941bbSAndroid Build Coastguard Worker      # flip horizontally
325*b7c941bbSAndroid Build Coastguard Worker      img = cv2.flip(img, 1)
326*b7c941bbSAndroid Build Coastguard Worker    else:
327*b7c941bbSAndroid Build Coastguard Worker      raise AssertionError('The captured image failed to detect the location '
328*b7c941bbSAndroid Build Coastguard Worker                           'of the brightest square.')
329*b7c941bbSAndroid Build Coastguard Worker  elif darkest_corner[0] == _KEY_TOP_RIGHT:
330*b7c941bbSAndroid Build Coastguard Worker    if brightest_corner[0] == _KEY_TOP_LEFT:
331*b7c941bbSAndroid Build Coastguard Worker      # flip vertically
332*b7c941bbSAndroid Build Coastguard Worker      img = cv2.flip(img, 0)
333*b7c941bbSAndroid Build Coastguard Worker    elif brightest_corner[0] == _KEY_BOTTOM_RIGHT:
334*b7c941bbSAndroid Build Coastguard Worker      # rotate 90 CW
335*b7c941bbSAndroid Build Coastguard Worker      img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
336*b7c941bbSAndroid Build Coastguard Worker    else:
337*b7c941bbSAndroid Build Coastguard Worker      raise AssertionError('The captured image failed to detect the location '
338*b7c941bbSAndroid Build Coastguard Worker                           'of the brightest square.')
339*b7c941bbSAndroid Build Coastguard Worker  elif darkest_corner[0] == _KEY_BOTTOM_RIGHT:
340*b7c941bbSAndroid Build Coastguard Worker    if brightest_corner[0] == _KEY_BOTTOM_LEFT:
341*b7c941bbSAndroid Build Coastguard Worker      # correct orientation
342*b7c941bbSAndroid Build Coastguard Worker      pass
343*b7c941bbSAndroid Build Coastguard Worker    elif brightest_corner[0] == _KEY_TOP_RIGHT:
344*b7c941bbSAndroid Build Coastguard Worker      # rotate 90 and flip horizontally
345*b7c941bbSAndroid Build Coastguard Worker      img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
346*b7c941bbSAndroid Build Coastguard Worker      img = cv2.flip(img, 1)
347*b7c941bbSAndroid Build Coastguard Worker    else:
348*b7c941bbSAndroid Build Coastguard Worker      raise AssertionError('The captured image failed to detect the location '
349*b7c941bbSAndroid Build Coastguard Worker                           'of the brightest square.')
350*b7c941bbSAndroid Build Coastguard Worker  return img
351*b7c941bbSAndroid Build Coastguard Worker
352*b7c941bbSAndroid Build Coastguard Worker
353*b7c941bbSAndroid Build Coastguard Workerdef _compute_luminance_regions(image, boxes):
354*b7c941bbSAndroid Build Coastguard Worker  """Compute the luminance for each box in scene_low_light.
355*b7c941bbSAndroid Build Coastguard Worker
356*b7c941bbSAndroid Build Coastguard Worker  Args:
357*b7c941bbSAndroid Build Coastguard Worker    image: numpy array; captured image.
358*b7c941bbSAndroid Build Coastguard Worker    boxes: array; array of boxes where each box is (x, y, w, h).
359*b7c941bbSAndroid Build Coastguard Worker  Returns:
360*b7c941bbSAndroid Build Coastguard Worker    Array of tuples where each tuple is (box, luminance).
361*b7c941bbSAndroid Build Coastguard Worker  """
362*b7c941bbSAndroid Build Coastguard Worker  intensities = []
363*b7c941bbSAndroid Build Coastguard Worker  for b in boxes:
364*b7c941bbSAndroid Build Coastguard Worker    x, y, w, h = b
365*b7c941bbSAndroid Build Coastguard Worker    padding = min(w, h) * _BOX_PADDING_RATIO
366*b7c941bbSAndroid Build Coastguard Worker    left = int(x + padding)
367*b7c941bbSAndroid Build Coastguard Worker    top = int(y + padding)
368*b7c941bbSAndroid Build Coastguard Worker    right = int(x + w - padding)
369*b7c941bbSAndroid Build Coastguard Worker    bottom = int(y + h - padding)
370*b7c941bbSAndroid Build Coastguard Worker    box = image[top:bottom, left:right]
371*b7c941bbSAndroid Build Coastguard Worker    box_xyz = cv2.cvtColor(box, cv2.COLOR_BGR2XYZ)
372*b7c941bbSAndroid Build Coastguard Worker    intensity = int(np.mean(box_xyz[1]))
373*b7c941bbSAndroid Build Coastguard Worker    intensities.append((b, intensity))
374*b7c941bbSAndroid Build Coastguard Worker  return intensities
375*b7c941bbSAndroid Build Coastguard Worker
376*b7c941bbSAndroid Build Coastguard Worker
377*b7c941bbSAndroid Build Coastguard Workerdef _draw_luminance(image, intensities):
378*b7c941bbSAndroid Build Coastguard Worker  """Draws the luma and noise for each box in scene_low_light for debugging.
379*b7c941bbSAndroid Build Coastguard Worker
380*b7c941bbSAndroid Build Coastguard Worker  Args:
381*b7c941bbSAndroid Build Coastguard Worker    image: numpy array; captured image.
382*b7c941bbSAndroid Build Coastguard Worker    intensities: array; array of tuples (box, luminance intensity).
383*b7c941bbSAndroid Build Coastguard Worker  """
384*b7c941bbSAndroid Build Coastguard Worker  for b, intensity in intensities:
385*b7c941bbSAndroid Build Coastguard Worker    x, y, w, h = b
386*b7c941bbSAndroid Build Coastguard Worker    padding = min(w, h) * _BOX_PADDING_RATIO
387*b7c941bbSAndroid Build Coastguard Worker    left = int(x + padding)
388*b7c941bbSAndroid Build Coastguard Worker    top = int(y + padding)
389*b7c941bbSAndroid Build Coastguard Worker    right = int(x + w - padding)
390*b7c941bbSAndroid Build Coastguard Worker    bottom = int(y + h - padding)
391*b7c941bbSAndroid Build Coastguard Worker    noise_stats = image_processing_utils.compute_patch_noise(
392*b7c941bbSAndroid Build Coastguard Worker        image, (left, top, (right - left), (bottom - top)))
393*b7c941bbSAndroid Build Coastguard Worker    cv2.rectangle(image, (left, top), (right, bottom), _BOUNDING_BOX_COLOR, 2)
394*b7c941bbSAndroid Build Coastguard Worker    # place the luma value above the box offset by 10 pixels
395*b7c941bbSAndroid Build Coastguard Worker    cv2.putText(img=image, text=f'{intensity}', org=(x, y - 10),
396*b7c941bbSAndroid Build Coastguard Worker                fontFace=cv2.FONT_HERSHEY_PLAIN, fontScale=1,
397*b7c941bbSAndroid Build Coastguard Worker                color=_TEXT_COLOR)
398*b7c941bbSAndroid Build Coastguard Worker    luma = str(round(noise_stats['luma'], 1))
399*b7c941bbSAndroid Build Coastguard Worker    cu = str(round(noise_stats['chroma_u'], 1))
400*b7c941bbSAndroid Build Coastguard Worker    cv = str(round(noise_stats['chroma_v'], 1))
401*b7c941bbSAndroid Build Coastguard Worker    # place the noise (luma, chroma u, chroma v) values above the luma value
402*b7c941bbSAndroid Build Coastguard Worker    # offset by 30 pixels
403*b7c941bbSAndroid Build Coastguard Worker    cv2.putText(img=image, text=f'{luma}, {cu}, {cv}', org=(x, y - 30),
404*b7c941bbSAndroid Build Coastguard Worker                fontFace=cv2.FONT_HERSHEY_PLAIN, fontScale=1,
405*b7c941bbSAndroid Build Coastguard Worker                color=_TEXT_COLOR)
406*b7c941bbSAndroid Build Coastguard Worker
407*b7c941bbSAndroid Build Coastguard Worker
408*b7c941bbSAndroid Build Coastguard Workerdef _compute_avg(results):
409*b7c941bbSAndroid Build Coastguard Worker  """Computes the average luminance of the first 6 boxes.
410*b7c941bbSAndroid Build Coastguard Worker
411*b7c941bbSAndroid Build Coastguard Worker  The boxes are part of scene_low_light.
412*b7c941bbSAndroid Build Coastguard Worker
413*b7c941bbSAndroid Build Coastguard Worker  Args:
414*b7c941bbSAndroid Build Coastguard Worker    results: A list of tuples where each tuple is (box, luminance).
415*b7c941bbSAndroid Build Coastguard Worker  Returns:
416*b7c941bbSAndroid Build Coastguard Worker    float; The average luminance of the first 6 boxes.
417*b7c941bbSAndroid Build Coastguard Worker  """
418*b7c941bbSAndroid Build Coastguard Worker  luminance_values = [luminance for _, luminance in results[:6]]
419*b7c941bbSAndroid Build Coastguard Worker  avg = sum(luminance_values) / len(luminance_values)
420*b7c941bbSAndroid Build Coastguard Worker  return avg
421*b7c941bbSAndroid Build Coastguard Worker
422*b7c941bbSAndroid Build Coastguard Worker
423*b7c941bbSAndroid Build Coastguard Workerdef _compute_avg_delta_of_successive_boxes(results):
424*b7c941bbSAndroid Build Coastguard Worker  """Computes the delta of successive boxes & takes the average of the first 5.
425*b7c941bbSAndroid Build Coastguard Worker
426*b7c941bbSAndroid Build Coastguard Worker  The boxes are part of scene_low_light.
427*b7c941bbSAndroid Build Coastguard Worker
428*b7c941bbSAndroid Build Coastguard Worker  Args:
429*b7c941bbSAndroid Build Coastguard Worker    results: A list of tuples where each tuple is (box, luminance).
430*b7c941bbSAndroid Build Coastguard Worker  Returns:
431*b7c941bbSAndroid Build Coastguard Worker    float; The average of the first 5 deltas of successive boxes.
432*b7c941bbSAndroid Build Coastguard Worker  """
433*b7c941bbSAndroid Build Coastguard Worker  luminance_values = [luminance for _, luminance in results[:6]]
434*b7c941bbSAndroid Build Coastguard Worker  delta = [luminance_values[i] - luminance_values[i - 1]
435*b7c941bbSAndroid Build Coastguard Worker           for i in range(1, len(luminance_values))]
436*b7c941bbSAndroid Build Coastguard Worker  avg = sum(delta) / len(delta)
437*b7c941bbSAndroid Build Coastguard Worker  return avg
438*b7c941bbSAndroid Build Coastguard Worker
439*b7c941bbSAndroid Build Coastguard Worker
440*b7c941bbSAndroid Build Coastguard Workerdef _plot_results(results, file_stem):
441*b7c941bbSAndroid Build Coastguard Worker  """Plots the computed luminance for each box in scene_low_light.
442*b7c941bbSAndroid Build Coastguard Worker
443*b7c941bbSAndroid Build Coastguard Worker  Args:
444*b7c941bbSAndroid Build Coastguard Worker    results: A list of tuples where each tuple is (box, luminance).
445*b7c941bbSAndroid Build Coastguard Worker    file_stem: The output file where the plot is saved.
446*b7c941bbSAndroid Build Coastguard Worker  """
447*b7c941bbSAndroid Build Coastguard Worker  luminance_values = [luminance for _, luminance in results]
448*b7c941bbSAndroid Build Coastguard Worker  box_labels = [f'Box {i + 1}' for i in range(len(results))]
449*b7c941bbSAndroid Build Coastguard Worker
450*b7c941bbSAndroid Build Coastguard Worker  plt.figure(figsize=_FIG_SIZE)
451*b7c941bbSAndroid Build Coastguard Worker  plt.plot(box_labels, luminance_values, marker='o', linestyle='-', color='b')
452*b7c941bbSAndroid Build Coastguard Worker  plt.scatter(box_labels, luminance_values, color='r')
453*b7c941bbSAndroid Build Coastguard Worker
454*b7c941bbSAndroid Build Coastguard Worker  plt.title('Luminance for each Box')
455*b7c941bbSAndroid Build Coastguard Worker  plt.xlabel('Boxes')
456*b7c941bbSAndroid Build Coastguard Worker  plt.ylabel('Luminance (pixel intensity)')
457*b7c941bbSAndroid Build Coastguard Worker  plt.grid('True')
458*b7c941bbSAndroid Build Coastguard Worker  plt.xticks(rotation=45)
459*b7c941bbSAndroid Build Coastguard Worker  plt.savefig(f'{file_stem}_luminance_plot.png', dpi=300)
460*b7c941bbSAndroid Build Coastguard Worker  plt.close()
461*b7c941bbSAndroid Build Coastguard Worker
462*b7c941bbSAndroid Build Coastguard Worker
463*b7c941bbSAndroid Build Coastguard Workerdef _plot_successive_difference(results, file_stem):
464*b7c941bbSAndroid Build Coastguard Worker  """Plots the successive difference in luminance between each box.
465*b7c941bbSAndroid Build Coastguard Worker
466*b7c941bbSAndroid Build Coastguard Worker  The boxes are part of scene_low_light.
467*b7c941bbSAndroid Build Coastguard Worker
468*b7c941bbSAndroid Build Coastguard Worker  Args:
469*b7c941bbSAndroid Build Coastguard Worker    results: A list of tuples where each tuple is (box, luminance).
470*b7c941bbSAndroid Build Coastguard Worker    file_stem: The output file where the plot is saved.
471*b7c941bbSAndroid Build Coastguard Worker  """
472*b7c941bbSAndroid Build Coastguard Worker  luminance_values = [luminance for _, luminance in results]
473*b7c941bbSAndroid Build Coastguard Worker  delta = [luminance_values[i] - luminance_values[i - 1]
474*b7c941bbSAndroid Build Coastguard Worker           for i in range(1, len(luminance_values))]
475*b7c941bbSAndroid Build Coastguard Worker  box_labels = [f'Box {i} to Box {i + 1}' for i in range(1, len(results))]
476*b7c941bbSAndroid Build Coastguard Worker
477*b7c941bbSAndroid Build Coastguard Worker  plt.figure(figsize=_FIG_SIZE)
478*b7c941bbSAndroid Build Coastguard Worker  plt.plot(box_labels, delta, marker='o', linestyle='-', color='b')
479*b7c941bbSAndroid Build Coastguard Worker  plt.scatter(box_labels, delta, color='r')
480*b7c941bbSAndroid Build Coastguard Worker
481*b7c941bbSAndroid Build Coastguard Worker  plt.title('Difference in Luminance Between Successive Boxes')
482*b7c941bbSAndroid Build Coastguard Worker  plt.xlabel('Box Transition')
483*b7c941bbSAndroid Build Coastguard Worker  plt.ylabel('Luminance Difference')
484*b7c941bbSAndroid Build Coastguard Worker  plt.grid('True')
485*b7c941bbSAndroid Build Coastguard Worker  plt.xticks(rotation=45)
486*b7c941bbSAndroid Build Coastguard Worker  plt.savefig(
487*b7c941bbSAndroid Build Coastguard Worker      f'{file_stem}_luminance_difference_between_successive_boxes_plot.png',
488*b7c941bbSAndroid Build Coastguard Worker      dpi=300)
489*b7c941bbSAndroid Build Coastguard Worker  plt.close()
490*b7c941bbSAndroid Build Coastguard Worker
491*b7c941bbSAndroid Build Coastguard Worker
492*b7c941bbSAndroid Build Coastguard Workerdef _plot_noise(results, file_stem, img, test_name):
493*b7c941bbSAndroid Build Coastguard Worker  """Plots the noise in the image.
494*b7c941bbSAndroid Build Coastguard Worker
495*b7c941bbSAndroid Build Coastguard Worker  The boxes are part of scene_low_light.
496*b7c941bbSAndroid Build Coastguard Worker
497*b7c941bbSAndroid Build Coastguard Worker  Args:
498*b7c941bbSAndroid Build Coastguard Worker    results: A list of tuples where each tuple is (box, luminance).
499*b7c941bbSAndroid Build Coastguard Worker    file_stem: The output file where the plot is saved.
500*b7c941bbSAndroid Build Coastguard Worker    img: The captured image used to measure patch noise.
501*b7c941bbSAndroid Build Coastguard Worker    test_name: Name of the test being plotted.
502*b7c941bbSAndroid Build Coastguard Worker  """
503*b7c941bbSAndroid Build Coastguard Worker  luma_noise_values = []
504*b7c941bbSAndroid Build Coastguard Worker  chroma_u_noise_values = []
505*b7c941bbSAndroid Build Coastguard Worker  chroma_v_noise_values = []
506*b7c941bbSAndroid Build Coastguard Worker  for region, _ in results:
507*b7c941bbSAndroid Build Coastguard Worker    x, y, w, h = region
508*b7c941bbSAndroid Build Coastguard Worker    padding = min(w, h) * _BOX_PADDING_RATIO
509*b7c941bbSAndroid Build Coastguard Worker    left = int(x + padding)
510*b7c941bbSAndroid Build Coastguard Worker    top = int(y + padding)
511*b7c941bbSAndroid Build Coastguard Worker    right = int(x + w - padding)
512*b7c941bbSAndroid Build Coastguard Worker    bottom = int(y + h - padding)
513*b7c941bbSAndroid Build Coastguard Worker    noise_stats = image_processing_utils.compute_patch_noise(
514*b7c941bbSAndroid Build Coastguard Worker        img, (left, top, (right - left), (bottom - top)))
515*b7c941bbSAndroid Build Coastguard Worker    luma_noise_values.append(noise_stats['luma'])
516*b7c941bbSAndroid Build Coastguard Worker    chroma_u_noise_values.append(noise_stats['chroma_u'])
517*b7c941bbSAndroid Build Coastguard Worker    chroma_v_noise_values.append(noise_stats['chroma_v'])
518*b7c941bbSAndroid Build Coastguard Worker
519*b7c941bbSAndroid Build Coastguard Worker  box_labels = [f'Box {i + 1}' for i in range(len(results))]
520*b7c941bbSAndroid Build Coastguard Worker
521*b7c941bbSAndroid Build Coastguard Worker  plt.figure(figsize=_FIG_SIZE)
522*b7c941bbSAndroid Build Coastguard Worker  plt.plot(box_labels, luma_noise_values, marker='o', linestyle='-',
523*b7c941bbSAndroid Build Coastguard Worker           color='b', label='luma')
524*b7c941bbSAndroid Build Coastguard Worker  plt.plot(box_labels, chroma_u_noise_values, marker='o', linestyle='-',
525*b7c941bbSAndroid Build Coastguard Worker           color='r', label='chroma u')
526*b7c941bbSAndroid Build Coastguard Worker  plt.plot(box_labels, chroma_v_noise_values, marker='o', linestyle='-',
527*b7c941bbSAndroid Build Coastguard Worker           color='g', label='chroma v')
528*b7c941bbSAndroid Build Coastguard Worker  plt.legend()
529*b7c941bbSAndroid Build Coastguard Worker
530*b7c941bbSAndroid Build Coastguard Worker  plt.title('Luma, Chroma U, and Chroma V Noise per Box')
531*b7c941bbSAndroid Build Coastguard Worker  plt.xlabel('Box')
532*b7c941bbSAndroid Build Coastguard Worker  plt.ylabel('Noise (std dev)')
533*b7c941bbSAndroid Build Coastguard Worker  plt.grid('True')
534*b7c941bbSAndroid Build Coastguard Worker  plt.xticks(rotation=45)
535*b7c941bbSAndroid Build Coastguard Worker  plt.savefig(f'{file_stem}_noise_per_box_plot.png', dpi=300)
536*b7c941bbSAndroid Build Coastguard Worker  plt.close()
537*b7c941bbSAndroid Build Coastguard Worker  # print the chart luma values for telemetry purposes
538*b7c941bbSAndroid Build Coastguard Worker  # do not convert to logging.debug
539*b7c941bbSAndroid Build Coastguard Worker  print(f'{test_name}_noise_luma: {luma_noise_values}')
540*b7c941bbSAndroid Build Coastguard Worker  print(f'{test_name}_noise_chroma_u: {chroma_u_noise_values}')
541*b7c941bbSAndroid Build Coastguard Worker  print(f'{test_name}_noise_chroma_v: {chroma_v_noise_values}')
542*b7c941bbSAndroid Build Coastguard Worker
543*b7c941bbSAndroid Build Coastguard Worker
544*b7c941bbSAndroid Build Coastguard Workerdef _sort_by_columns(regions):
545*b7c941bbSAndroid Build Coastguard Worker  """Sort the regions by columns and then by row within each column.
546*b7c941bbSAndroid Build Coastguard Worker
547*b7c941bbSAndroid Build Coastguard Worker  These regions are part of scene_low_light.
548*b7c941bbSAndroid Build Coastguard Worker
549*b7c941bbSAndroid Build Coastguard Worker  Args:
550*b7c941bbSAndroid Build Coastguard Worker    regions: The tuple of (box, luminance) of each square.
551*b7c941bbSAndroid Build Coastguard Worker  Returns:
552*b7c941bbSAndroid Build Coastguard Worker    array; an array of tuples of (box, luminance) sorted by columns then by row
553*b7c941bbSAndroid Build Coastguard Worker      within each column.
554*b7c941bbSAndroid Build Coastguard Worker  """
555*b7c941bbSAndroid Build Coastguard Worker  # The input is 20 elements. The first two and last two elements represent the
556*b7c941bbSAndroid Build Coastguard Worker  # 4 boxes on the outside used for diagnostics. Boxes in indices 2 through 17
557*b7c941bbSAndroid Build Coastguard Worker  # represent the elements in the 4x4 grid.
558*b7c941bbSAndroid Build Coastguard Worker
559*b7c941bbSAndroid Build Coastguard Worker  # Sort all elements by column
560*b7c941bbSAndroid Build Coastguard Worker  col_sorted = sorted(regions, key=lambda r: r[0][0])
561*b7c941bbSAndroid Build Coastguard Worker
562*b7c941bbSAndroid Build Coastguard Worker  # Sort elements within each column by row
563*b7c941bbSAndroid Build Coastguard Worker  result = []
564*b7c941bbSAndroid Build Coastguard Worker  result.extend(sorted(col_sorted[:2], key=lambda r: r[0][1]))
565*b7c941bbSAndroid Build Coastguard Worker
566*b7c941bbSAndroid Build Coastguard Worker  for i in range(4):
567*b7c941bbSAndroid Build Coastguard Worker    # take 4 rows per column and then sort the rows
568*b7c941bbSAndroid Build Coastguard Worker    # skip the first two elements
569*b7c941bbSAndroid Build Coastguard Worker    offset = i*4+2
570*b7c941bbSAndroid Build Coastguard Worker    col = col_sorted[offset:(offset+4)]
571*b7c941bbSAndroid Build Coastguard Worker    result.extend(sorted(col, key=lambda r: r[0][1]))
572*b7c941bbSAndroid Build Coastguard Worker
573*b7c941bbSAndroid Build Coastguard Worker  result.extend(sorted(col_sorted[-2:], key=lambda r: r[0][1]))
574*b7c941bbSAndroid Build Coastguard Worker  return result
575*b7c941bbSAndroid Build Coastguard Worker
576*b7c941bbSAndroid Build Coastguard Worker
577*b7c941bbSAndroid Build Coastguard Workerdef analyze_low_light_scene_capture(
578*b7c941bbSAndroid Build Coastguard Worker    file_stem,
579*b7c941bbSAndroid Build Coastguard Worker    img,
580*b7c941bbSAndroid Build Coastguard Worker    avg_luminance_threshold=_LOW_LIGHT_BOOST_AVG_LUMINANCE_THRESH,
581*b7c941bbSAndroid Build Coastguard Worker    avg_delta_luminance_threshold=_LOW_LIGHT_BOOST_AVG_DELTA_LUMINANCE_THRESH):
582*b7c941bbSAndroid Build Coastguard Worker  """Analyze a captured frame to check if it meets low light scene criteria.
583*b7c941bbSAndroid Build Coastguard Worker
584*b7c941bbSAndroid Build Coastguard Worker  The capture is cropped first, then detects for boxes, and then computes the
585*b7c941bbSAndroid Build Coastguard Worker  luminance of each box.
586*b7c941bbSAndroid Build Coastguard Worker
587*b7c941bbSAndroid Build Coastguard Worker  Args:
588*b7c941bbSAndroid Build Coastguard Worker    file_stem: The file prefix for results saved.
589*b7c941bbSAndroid Build Coastguard Worker    img: numpy array; The captured image loaded by cv2 as and available for
590*b7c941bbSAndroid Build Coastguard Worker      analysis.
591*b7c941bbSAndroid Build Coastguard Worker    avg_luminance_threshold: minimum average luminance of the first 6 boxes.
592*b7c941bbSAndroid Build Coastguard Worker    avg_delta_luminance_threshold: minimum average difference in luminance
593*b7c941bbSAndroid Build Coastguard Worker      of the first 5 successive boxes of luminance.
594*b7c941bbSAndroid Build Coastguard Worker  """
595*b7c941bbSAndroid Build Coastguard Worker  cv2.imwrite(f'{file_stem}_original.jpg', img)
596*b7c941bbSAndroid Build Coastguard Worker  img = _crop(img)
597*b7c941bbSAndroid Build Coastguard Worker  cv2.imwrite(f'{file_stem}_cropped.jpg', img)
598*b7c941bbSAndroid Build Coastguard Worker  boxes = _find_boxes(img)
599*b7c941bbSAndroid Build Coastguard Worker  if len(boxes) != _EXPECTED_NUM_OF_BOXES:
600*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('The captured image failed to detect the expected '
601*b7c941bbSAndroid Build Coastguard Worker                         'number of boxes. '
602*b7c941bbSAndroid Build Coastguard Worker                         'Check the captured image to see if the image was '
603*b7c941bbSAndroid Build Coastguard Worker                         'correctly captured and try again. '
604*b7c941bbSAndroid Build Coastguard Worker                         f'Actual: {len(boxes)}, '
605*b7c941bbSAndroid Build Coastguard Worker                         f'Expected: {_EXPECTED_NUM_OF_BOXES}')
606*b7c941bbSAndroid Build Coastguard Worker
607*b7c941bbSAndroid Build Coastguard Worker  regions = _compute_luminance_regions(img, boxes)
608*b7c941bbSAndroid Build Coastguard Worker
609*b7c941bbSAndroid Build Coastguard Worker  # Sorted so each column is read left to right
610*b7c941bbSAndroid Build Coastguard Worker  sorted_regions = _sort_by_columns(regions)
611*b7c941bbSAndroid Build Coastguard Worker  img = _correct_image_rotation(img, sorted_regions)
612*b7c941bbSAndroid Build Coastguard Worker  cv2.imwrite(f'{file_stem}_rotated.jpg', img)
613*b7c941bbSAndroid Build Coastguard Worker
614*b7c941bbSAndroid Build Coastguard Worker  # The orientation of the image may have changed which will affect the
615*b7c941bbSAndroid Build Coastguard Worker  # coordinates of the squares. Therefore, locate the squares, recompute the
616*b7c941bbSAndroid Build Coastguard Worker  # regions, and sort again
617*b7c941bbSAndroid Build Coastguard Worker  boxes = _find_boxes(img)
618*b7c941bbSAndroid Build Coastguard Worker  regions = _compute_luminance_regions(img, boxes)
619*b7c941bbSAndroid Build Coastguard Worker  sorted_regions = _sort_by_columns(regions)
620*b7c941bbSAndroid Build Coastguard Worker
621*b7c941bbSAndroid Build Coastguard Worker  # Reorder this so the regions are increasing in luminance according to the
622*b7c941bbSAndroid Build Coastguard Worker  # Hilbert curve arrangement pattern of the grid
623*b7c941bbSAndroid Build Coastguard Worker  # See scene_low_light_reference.png which indicates the order of each
624*b7c941bbSAndroid Build Coastguard Worker  # box
625*b7c941bbSAndroid Build Coastguard Worker  hilbert_ordered = [
626*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[17],
627*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[13],
628*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[12],
629*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[16],
630*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[15],
631*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[14],
632*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[10],
633*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[11],
634*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[7],
635*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[6],
636*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[2],
637*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[3],
638*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[4],
639*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[8],
640*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[9],
641*b7c941bbSAndroid Build Coastguard Worker      sorted_regions[5],
642*b7c941bbSAndroid Build Coastguard Worker  ]
643*b7c941bbSAndroid Build Coastguard Worker
644*b7c941bbSAndroid Build Coastguard Worker  test_name = os.path.basename(file_stem)
645*b7c941bbSAndroid Build Coastguard Worker
646*b7c941bbSAndroid Build Coastguard Worker  _plot_results(hilbert_ordered, file_stem)
647*b7c941bbSAndroid Build Coastguard Worker  _plot_successive_difference(hilbert_ordered, file_stem)
648*b7c941bbSAndroid Build Coastguard Worker  _plot_noise(hilbert_ordered, file_stem, img, test_name)
649*b7c941bbSAndroid Build Coastguard Worker
650*b7c941bbSAndroid Build Coastguard Worker  _draw_luminance(img, regions)
651*b7c941bbSAndroid Build Coastguard Worker  cv2.imwrite(f'{file_stem}_result.jpg', img)
652*b7c941bbSAndroid Build Coastguard Worker
653*b7c941bbSAndroid Build Coastguard Worker  avg = _compute_avg(hilbert_ordered)
654*b7c941bbSAndroid Build Coastguard Worker  delta_avg = _compute_avg_delta_of_successive_boxes(hilbert_ordered)
655*b7c941bbSAndroid Build Coastguard Worker
656*b7c941bbSAndroid Build Coastguard Worker  # the following print statements are necessary for telemetry
657*b7c941bbSAndroid Build Coastguard Worker  # do not convert to logging.debug
658*b7c941bbSAndroid Build Coastguard Worker  print(f'{test_name}_avg_luma: {avg:.2f}')
659*b7c941bbSAndroid Build Coastguard Worker  print(f'{test_name}_delta_avg_luma: {delta_avg:.2f}')
660*b7c941bbSAndroid Build Coastguard Worker  chart_luma_values = [v[1] for v in hilbert_ordered]
661*b7c941bbSAndroid Build Coastguard Worker  print(f'{test_name}_chart_luma: {chart_luma_values}')
662*b7c941bbSAndroid Build Coastguard Worker
663*b7c941bbSAndroid Build Coastguard Worker  logging.debug('average luminance of the 6 boxes: %.2f', avg)
664*b7c941bbSAndroid Build Coastguard Worker  logging.debug('average difference in luminance of 5 successive boxes: %.2f',
665*b7c941bbSAndroid Build Coastguard Worker                delta_avg)
666*b7c941bbSAndroid Build Coastguard Worker  if avg < float(avg_luminance_threshold):
667*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('Average luminance of the first 6 boxes did not '
668*b7c941bbSAndroid Build Coastguard Worker                         'meet minimum requirements for low light scene '
669*b7c941bbSAndroid Build Coastguard Worker                         'criteria. '
670*b7c941bbSAndroid Build Coastguard Worker                         f'Actual: {avg:.2f}, '
671*b7c941bbSAndroid Build Coastguard Worker                         f'Expected: {avg_luminance_threshold}')
672*b7c941bbSAndroid Build Coastguard Worker  if delta_avg < float(avg_delta_luminance_threshold):
673*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('The average difference in luminance of the first 5 '
674*b7c941bbSAndroid Build Coastguard Worker                         'successive boxes did not meet minimum requirements '
675*b7c941bbSAndroid Build Coastguard Worker                         'for low light scene criteria. '
676*b7c941bbSAndroid Build Coastguard Worker                         f'Actual: {delta_avg:.2f}, '
677*b7c941bbSAndroid Build Coastguard Worker                         f'Expected: {avg_delta_luminance_threshold}')
678