• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Verifies android.jpeg.quality increases JPEG image quality."""
15
16
17import logging
18import math
19import os.path
20
21from matplotlib import pylab
22import matplotlib.pyplot
23from mobly import test_runner
24import numpy as np
25
26import its_base_test
27import camera_properties_utils
28import capture_request_utils
29import image_processing_utils
30import its_session_utils
31
32JPEG_APPN_MARKERS = [[255, 224], [255, 225], [255, 226], [255, 227], [255, 228],
33                     [255, 229], [255, 230], [255, 231], [255, 232], [255, 235]]
34JPEG_DHT_MARKER = [255, 196]  # JPEG Define Huffman Table
35JPEG_DQT_MARKER = [255, 219]  # JPEG Define Quantization Table
36JPEG_DQT_TOL = 0.8  # -20% for each +20 in jpeg.quality (empirical number)
37JPEG_EOI_MARKER = [255, 217]  # JPEG End of Image
38JPEG_SOI_MARKER = [255, 216]  # JPEG Start of Image
39JPEG_SOS_MARKER = [255, 218]  # JPEG Start of Scan
40NAME = os.path.splitext(os.path.basename(__file__))[0]
41QUALITIES = [25, 45, 65, 85]
42SYMBOLS = ['o', 's', 'v', '^', '<', '>']
43
44
45def is_square(integer):
46  root = math.sqrt(integer)
47  return integer == int(root + 0.5)**2
48
49
50def strip_soi_marker(jpeg):
51  """Strip off start of image marker.
52
53  SOI is of form [xFF xD8] and JPEG needs to start with marker.
54
55  Args:
56   jpeg: 1-D numpy int [0:255] array; values from JPEG capture
57
58  Returns:
59    jpeg with SOI marker stripped off.
60  """
61
62  soi = jpeg[0:2]
63  if list(soi) != JPEG_SOI_MARKER:
64    raise AssertionError('JPEG has no Start Of Image marker')
65  return jpeg[2:]
66
67
68def strip_appn_data(jpeg):
69  """Strip off application specific data at beginning of JPEG.
70
71  APPN markers are of form [xFF, xE*, size_msb, size_lsb] and should follow
72  SOI marker.
73
74  Args:
75   jpeg: 1-D numpy int [0:255] array; values from JPEG capture
76
77  Returns:
78    jpeg with APPN marker(s) and data stripped off.
79  """
80
81  length = 0
82  i = 0
83  # find APPN markers and strip off payloads at beginning of jpeg
84  while i < len(jpeg) - 1:
85    if [jpeg[i], jpeg[i + 1]] in JPEG_APPN_MARKERS:
86      length = jpeg[i + 2] * 256 + jpeg[i + 3] + 2
87      logging.debug('stripped APPN length:%d', length)
88      jpeg = np.concatenate((jpeg[0:i], jpeg[length:]), axis=None)
89    elif ([jpeg[i], jpeg[i + 1]] == JPEG_DQT_MARKER or
90          [jpeg[i], jpeg[i + 1]] == JPEG_DHT_MARKER):
91      break
92    else:
93      i += 1
94
95  return jpeg
96
97
98def find_dqt_markers(marker, jpeg):
99  """Find location(s) of marker list in jpeg.
100
101  DQT marker is of form [xFF, xDB].
102
103  Args:
104    marker: list; marker values
105    jpeg: 1-D numpy int [0:255] array; JPEG capture w/ SOI & APPN stripped
106
107  Returns:
108    locs: list; marker locations in jpeg
109  """
110  locs = []
111  marker_len = len(marker)
112  for i in range(len(jpeg) - marker_len + 1):
113    if list(jpeg[i:i + marker_len]) == marker:
114      locs.append(i)
115  return locs
116
117
118def extract_dqts(jpeg, debug=False):
119  """Find and extract the DQT info in the JPEG.
120
121  SOI marker and APPN markers plus data are stripped off front of JPEG.
122  DQT marker is of form [xFF, xDB] followed by [size_msb, size_lsb].
123  Size includes the size values, but not the marker values.
124  Luma DQT is prefixed by 0, Chroma DQT by 1.
125  DQTs can have both luma & chroma or each individually.
126  There can be more than one DQT table for luma and chroma.
127
128  Args:
129   jpeg: 1-D numpy int [0:255] array; values from JPEG capture
130   debug: bool; command line flag to print debug data
131
132  Returns:
133    lumas,chromas: lists of numpy means of luma & chroma DQT matrices.
134    Higher values represent higher compression.
135  """
136
137  dqt_markers = find_dqt_markers(JPEG_DQT_MARKER, jpeg)
138  logging.debug('DQT header loc(s):%s', dqt_markers)
139  lumas = []
140  chromas = []
141  for i, dqt in enumerate(dqt_markers):
142    if debug:
143      logging.debug('DQT %d start: %d, marker: %s, length: %s', i, dqt,
144                    jpeg[dqt:dqt + 2], jpeg[dqt + 2:dqt + 4])
145    dqt_size = jpeg[dqt + 2] * 256 + jpeg[dqt + 3] - 2  # strip off size marker
146    if dqt_size % 2 == 0:  # even payload means luma & chroma
147      logging.debug(' both luma & chroma DQT matrices in marker')
148      dqt_size = (dqt_size - 2) // 2  # subtact off luma/chroma markers
149      if not is_square(dqt_size):
150        raise AssertionError(f'DQT size: {dqt_size}')
151      luma_start = dqt + 5  # skip header, length, & matrix id
152      chroma_start = luma_start + dqt_size + 1  # skip lumen &  matrix_id
153      luma = np.array(jpeg[luma_start: luma_start + dqt_size])
154      chroma = np.array(jpeg[chroma_start: chroma_start + dqt_size])
155      lumas.append(np.mean(luma))
156      chromas.append(np.mean(chroma))
157      if debug:
158        h = int(math.sqrt(dqt_size))
159        logging.debug(' luma:%s', luma.reshape(h, h))
160        logging.debug(' chroma:%s', chroma.reshape(h, h))
161    else:  # odd payload means only 1 matrix
162      logging.debug(' single DQT matrix in marker')
163      dqt_size = dqt_size - 1  # subtract off luma/chroma marker
164      if not is_square(dqt_size):
165        raise AssertionError(f'DQT size: {dqt_size}')
166      start = dqt + 5
167      matrix = np.array(jpeg[start:start + dqt_size])
168      if jpeg[dqt + 4]:  # chroma == 1
169        chromas.append(np.mean(matrix))
170        if debug:
171          h = int(math.sqrt(dqt_size))
172          logging.debug(' chroma:%s', matrix.reshape(h, h))
173      else:  # luma == 0
174        lumas.append(np.mean(matrix))
175        if debug:
176          h = int(math.sqrt(dqt_size))
177          logging.debug(' luma:%s', matrix.reshape(h, h))
178
179  return lumas, chromas
180
181
182def plot_data(qualities, lumas, chromas, img_name):
183  """Create plot of data."""
184  logging.debug('qualities: %s', str(qualities))
185  logging.debug('luma DQT avgs: %s', str(lumas))
186  logging.debug('chroma DQT avgs: %s', str(chromas))
187  pylab.title(NAME)
188  for i in range(lumas.shape[1]):
189    pylab.plot(
190        qualities, lumas[:, i], '-g' + SYMBOLS[i], label='luma_dqt' + str(i))
191    pylab.plot(
192        qualities,
193        chromas[:, i],
194        '-r' + SYMBOLS[i],
195        label='chroma_dqt' + str(i))
196  pylab.xlim([0, 100])
197  pylab.ylim([0, None])
198  pylab.xlabel('jpeg.quality')
199  pylab.ylabel('DQT luma/chroma matrix averages')
200  pylab.legend(loc='upper right', numpoints=1, fancybox=True)
201  matplotlib.pyplot.savefig('%s_plot.png' % img_name)
202
203
204class JpegQualityTest(its_base_test.ItsBaseTest):
205  """Test the camera JPEG compression quality.
206
207  Step JPEG qualities through android.jpeg.quality. Ensure quanitization
208  matrix decreases with quality increase. Matrix should decrease as the
209  matrix represents the division factor. Higher numbers --> fewer quantization
210  levels.
211  """
212
213  def test_jpeg_quality(self):
214    logging.debug('Starting %s', NAME)
215    # init variables
216    lumas = []
217    chromas = []
218
219    with its_session_utils.ItsSession(
220        device_id=self.dut.serial,
221        camera_id=self.camera_id,
222        hidden_physical_id=self.hidden_physical_id) as cam:
223
224      props = cam.get_camera_properties()
225      props = cam.override_with_hidden_physical_camera_props(props)
226      debug = self.debug_mode
227
228      # Load chart for scene
229      its_session_utils.load_scene(
230          cam, props, self.scene, self.tablet, self.chart_distance)
231
232      # Check skip conditions
233      camera_properties_utils.skip_unless(
234          camera_properties_utils.jpeg_quality(props))
235      cam.do_3a()
236
237      # do captures over jpeg quality range
238      req = capture_request_utils.auto_capture_request()
239      for q in QUALITIES:
240        logging.debug('jpeg.quality: %.d', q)
241        req['android.jpeg.quality'] = q
242        cap = cam.do_capture(req, cam.CAP_JPEG)
243        jpeg = cap['data']
244
245        # strip off start of image
246        jpeg = strip_soi_marker(jpeg)
247
248        # strip off application specific data
249        jpeg = strip_appn_data(jpeg)
250        logging.debug('remaining JPEG header:%s', jpeg[0:4])
251
252        # find and extract DQTs
253        lumas_i, chromas_i = extract_dqts(jpeg, debug)
254        lumas.append(lumas_i)
255        chromas.append(chromas_i)
256
257        # save JPEG image
258        img = image_processing_utils.convert_capture_to_rgb_image(
259            cap, props=props)
260        img_name = os.path.join(self.log_path, NAME)
261        image_processing_utils.write_image(img, '%s_%d.jpg' % (img_name, q))
262
263    # turn lumas/chromas into np array to ease multi-dimensional plots/asserts
264    lumas = np.array(lumas)
265    chromas = np.array(chromas)
266
267    # create plot of luma & chroma averages vs quality
268    plot_data(QUALITIES, lumas, chromas, img_name)
269
270    # assert decreasing luma/chroma with improved jpeg quality
271    for i in range(lumas.shape[1]):
272      l = lumas[:, i]
273      c = chromas[:, i]
274      if not all(y < x * JPEG_DQT_TOL for x, y in zip(l, l[1:])):
275        raise AssertionError(f'luma DQT avgs: {l}, TOL: {JPEG_DQT_TOL}')
276
277      if not all(y < x * JPEG_DQT_TOL for x, y in zip(c, c[1:])):
278        raise AssertionError(f'chroma DQT avgs: {c}, TOL: {JPEG_DQT_TOL}')
279
280if __name__ == '__main__':
281  test_runner.main()
282