blob: c4d91b4d9f47e9f6dfd6f2e1a2109aca4d10f1a5 [file] [log] [blame]
Clemenz Portmann813d3962020-03-06 15:15:29 -08001# 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
Clemenz Portmann813d3962020-03-06 15:15:29 -080013# limitations under the License.
Clemenz Portmanndc0af422021-03-25 14:34:27 -070014"""Verifies android.jpeg.quality increases JPEG image quality."""
Clemenz Portmann813d3962020-03-06 15:15:29 -080015
Clemenz Portmann6816c022021-03-01 13:00:12 -080016
17import logging
Clemenz Portmann813d3962020-03-06 15:15:29 -080018import math
19import os.path
20
Clemenz Portmann813d3962020-03-06 15:15:29 -080021from matplotlib import pylab
22import matplotlib.pyplot
Clemenz Portmann6816c022021-03-01 13:00:12 -080023from mobly import test_runner
Clemenz Portmann813d3962020-03-06 15:15:29 -080024import numpy as np
25
Clemenz Portmann6816c022021-03-01 13:00:12 -080026import its_base_test
27import camera_properties_utils
28import capture_request_utils
29import image_processing_utils
30import its_session_utils
31
Clemenz Portmann813d3962020-03-06 15:15:29 -080032JPEG_APPN_MARKERS = [[255, 224], [255, 225], [255, 226], [255, 227], [255, 228],
Clemenz Portmanna6a17452021-01-07 09:43:24 -080033 [255, 229], [255, 230], [255, 231], [255, 232], [255, 235]]
Clemenz Portmann813d3962020-03-06 15:15:29 -080034JPEG_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
Clemenz Portmann6816c022021-03-01 13:00:12 -080040NAME = os.path.splitext(os.path.basename(__file__))[0]
Clemenz Portmann813d3962020-03-06 15:15:29 -080041QUALITIES = [25, 45, 65, 85]
42SYMBOLS = ['o', 's', 'v', '^', '<', '>']
43
44
45def is_square(integer):
Clemenz Portmann6816c022021-03-01 13:00:12 -080046 root = math.sqrt(integer)
47 return integer == int(root + 0.5)**2
Clemenz Portmann813d3962020-03-06 15:15:29 -080048
49
50def strip_soi_marker(jpeg):
Clemenz Portmann6816c022021-03-01 13:00:12 -080051 """Strip off start of image marker.
Clemenz Portmann813d3962020-03-06 15:15:29 -080052
Clemenz Portmann6816c022021-03-01 13:00:12 -080053 SOI is of form [xFF xD8] and JPEG needs to start with marker.
Clemenz Portmann813d3962020-03-06 15:15:29 -080054
Clemenz Portmann6816c022021-03-01 13:00:12 -080055 Args:
56 jpeg: 1-D numpy int [0:255] array; values from JPEG capture
Clemenz Portmann813d3962020-03-06 15:15:29 -080057
Clemenz Portmann6816c022021-03-01 13:00:12 -080058 Returns:
59 jpeg with SOI marker stripped off.
60 """
Clemenz Portmann813d3962020-03-06 15:15:29 -080061
Clemenz Portmann6816c022021-03-01 13:00:12 -080062 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:]
Clemenz Portmann813d3962020-03-06 15:15:29 -080066
67
68def strip_appn_data(jpeg):
Clemenz Portmann6816c022021-03-01 13:00:12 -080069 """Strip off application specific data at beginning of JPEG.
Clemenz Portmann813d3962020-03-06 15:15:29 -080070
Clemenz Portmann6816c022021-03-01 13:00:12 -080071 APPN markers are of form [xFF, xE*, size_msb, size_lsb] and should follow
72 SOI marker.
Clemenz Portmann813d3962020-03-06 15:15:29 -080073
Clemenz Portmann6816c022021-03-01 13:00:12 -080074 Args:
75 jpeg: 1-D numpy int [0:255] array; values from JPEG capture
Clemenz Portmann813d3962020-03-06 15:15:29 -080076
Clemenz Portmann6816c022021-03-01 13:00:12 -080077 Returns:
78 jpeg with APPN marker(s) and data stripped off.
79 """
Clemenz Portmann813d3962020-03-06 15:15:29 -080080
Clemenz Portmann6816c022021-03-01 13:00:12 -080081 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
Clemenz Portmann813d3962020-03-06 15:15:29 -080094
Clemenz Portmann6816c022021-03-01 13:00:12 -080095 return jpeg
Clemenz Portmann813d3962020-03-06 15:15:29 -080096
97
98def find_dqt_markers(marker, jpeg):
Clemenz Portmann6816c022021-03-01 13:00:12 -080099 """Find location(s) of marker list in jpeg.
Clemenz Portmann813d3962020-03-06 15:15:29 -0800100
Clemenz Portmann6816c022021-03-01 13:00:12 -0800101 DQT marker is of form [xFF, xDB].
Clemenz Portmann813d3962020-03-06 15:15:29 -0800102
Clemenz Portmann6816c022021-03-01 13:00:12 -0800103 Args:
104 marker: list; marker values
105 jpeg: 1-D numpy int [0:255] array; JPEG capture w/ SOI & APPN stripped
Clemenz Portmann813d3962020-03-06 15:15:29 -0800106
Clemenz Portmann6816c022021-03-01 13:00:12 -0800107 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
Clemenz Portmann813d3962020-03-06 15:15:29 -0800116
117
118def extract_dqts(jpeg, debug=False):
Clemenz Portmann6816c022021-03-01 13:00:12 -0800119 """Find and extract the DQT info in the JPEG.
Clemenz Portmann813d3962020-03-06 15:15:29 -0800120
Clemenz Portmann6816c022021-03-01 13:00:12 -0800121 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.
Clemenz Portmann813d3962020-03-06 15:15:29 -0800127
Clemenz Portmann6816c022021-03-01 13:00:12 -0800128 Args:
129 jpeg: 1-D numpy int [0:255] array; values from JPEG capture
130 debug: bool; command line flag to print debug data
Clemenz Portmann813d3962020-03-06 15:15:29 -0800131
Clemenz Portmann6816c022021-03-01 13:00:12 -0800132 Returns:
133 lumas,chromas: lists of numpy means of luma & chroma DQT matrices.
134 Higher values represent higher compression.
135 """
Clemenz Portmann813d3962020-03-06 15:15:29 -0800136
Clemenz Portmann6816c022021-03-01 13:00:12 -0800137 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 assert is_square(dqt_size), 'DQT size: %d' % dqt_size
150 luma_start = dqt + 5 # skip header, length, & matrix id
151 chroma_start = luma_start + dqt_size + 1 # skip lumen & matrix_id
152 luma = np.array(jpeg[luma_start: luma_start + dqt_size])
153 chroma = np.array(jpeg[chroma_start: chroma_start + dqt_size])
154 lumas.append(np.mean(luma))
155 chromas.append(np.mean(chroma))
156 if debug:
157 h = int(math.sqrt(dqt_size))
158 logging.debug(' luma:%s', luma.reshape(h, h))
159 logging.debug(' chroma:%s', chroma.reshape(h, h))
160 else: # odd payload means only 1 matrix
161 logging.debug(' single DQT matrix in marker')
162 dqt_size = dqt_size - 1 # subtract off luma/chroma marker
163 if not is_square(dqt_size):
164 raise AssertionError(f'DQT size: {dqt_size}')
165 start = dqt + 5
166 matrix = np.array(jpeg[start:start + dqt_size])
167 if jpeg[dqt + 4]: # chroma == 1
168 chromas.append(np.mean(matrix))
Clemenz Portmann813d3962020-03-06 15:15:29 -0800169 if debug:
Clemenz Portmann6816c022021-03-01 13:00:12 -0800170 h = int(math.sqrt(dqt_size))
171 logging.debug(' chroma:%s', matrix.reshape(h, h))
172 else: # luma == 0
173 lumas.append(np.mean(matrix))
174 if debug:
175 h = int(math.sqrt(dqt_size))
176 logging.debug(' luma:%s', matrix.reshape(h, h))
Clemenz Portmann813d3962020-03-06 15:15:29 -0800177
Clemenz Portmann6816c022021-03-01 13:00:12 -0800178 return lumas, chromas
Clemenz Portmann813d3962020-03-06 15:15:29 -0800179
180
Clemenz Portmann6816c022021-03-01 13:00:12 -0800181def plot_data(qualities, lumas, chromas, img_name):
182 """Create plot of data."""
183 logging.debug('qualities: %s', str(qualities))
184 logging.debug('luma DQT avgs: %s', str(lumas))
185 logging.debug('chroma DQT avgs: %s', str(chromas))
186 pylab.title(NAME)
187 for i in range(lumas.shape[1]):
188 pylab.plot(
189 qualities, lumas[:, i], '-g' + SYMBOLS[i], label='luma_dqt' + str(i))
190 pylab.plot(
191 qualities,
192 chromas[:, i],
193 '-r' + SYMBOLS[i],
194 label='chroma_dqt' + str(i))
195 pylab.xlim([0, 100])
196 pylab.ylim([0, None])
197 pylab.xlabel('jpeg.quality')
198 pylab.ylabel('DQT luma/chroma matrix averages')
199 pylab.legend(loc='upper right', numpoints=1, fancybox=True)
200 matplotlib.pyplot.savefig('%s_plot.png' % img_name)
Clemenz Portmann813d3962020-03-06 15:15:29 -0800201
202
Clemenz Portmann6816c022021-03-01 13:00:12 -0800203class JpegQualityTest(its_base_test.ItsBaseTest):
204 """Test the camera JPEG compression quality.
Clemenz Portmann813d3962020-03-06 15:15:29 -0800205
Clemenz Portmann6816c022021-03-01 13:00:12 -0800206 Step JPEG qualities through android.jpeg.quality. Ensure quanitization
207 matrix decreases with quality increase. Matrix should decrease as the
208 matrix represents the division factor. Higher numbers --> fewer quantization
209 levels.
210 """
Clemenz Portmann813d3962020-03-06 15:15:29 -0800211
Clemenz Portmann6816c022021-03-01 13:00:12 -0800212 def test_jpeg_quality(self):
213 logging.debug('Starting %s', NAME)
Clemenz Portmann813d3962020-03-06 15:15:29 -0800214 # init variables
215 lumas = []
216 chromas = []
217
Clemenz Portmann6816c022021-03-01 13:00:12 -0800218 with its_session_utils.ItsSession(
219 device_id=self.dut.serial,
220 camera_id=self.camera_id,
221 hidden_physical_id=self.hidden_physical_id) as cam:
Clemenz Portmann813d3962020-03-06 15:15:29 -0800222
Clemenz Portmann6816c022021-03-01 13:00:12 -0800223 props = cam.get_camera_properties()
224 props = cam.override_with_hidden_physical_camera_props(props)
225 debug = self.debug_mode
Clemenz Portmann813d3962020-03-06 15:15:29 -0800226
Clemenz Portmann6816c022021-03-01 13:00:12 -0800227 # Load chart for scene
228 its_session_utils.load_scene(
229 cam, props, self.scene, self.tablet, self.chart_distance)
Clemenz Portmann813d3962020-03-06 15:15:29 -0800230
Clemenz Portmann6816c022021-03-01 13:00:12 -0800231 # Check skip conditions
232 camera_properties_utils.skip_unless(
233 camera_properties_utils.jpeg_quality(props))
234 cam.do_3a()
Clemenz Portmann813d3962020-03-06 15:15:29 -0800235
Clemenz Portmann6816c022021-03-01 13:00:12 -0800236 # do captures over jpeg quality range
237 req = capture_request_utils.auto_capture_request()
238 for q in QUALITIES:
239 logging.debug('jpeg.quality: %.d', q)
240 req['android.jpeg.quality'] = q
241 cap = cam.do_capture(req, cam.CAP_JPEG)
242 jpeg = cap['data']
Clemenz Portmann813d3962020-03-06 15:15:29 -0800243
Clemenz Portmann6816c022021-03-01 13:00:12 -0800244 # strip off start of image
245 jpeg = strip_soi_marker(jpeg)
246
247 # strip off application specific data
248 jpeg = strip_appn_data(jpeg)
249 logging.debug('remaining JPEG header:%s', jpeg[0:4])
250
251 # find and extract DQTs
252 lumas_i, chromas_i = extract_dqts(jpeg, debug)
253 lumas.append(lumas_i)
254 chromas.append(chromas_i)
255
256 # save JPEG image
257 img = image_processing_utils.convert_capture_to_rgb_image(
258 cap, props=props)
259 img_name = os.path.join(self.log_path, NAME)
260 image_processing_utils.write_image(img, '%s_%d.jpg' % (img_name, q))
Clemenz Portmann813d3962020-03-06 15:15:29 -0800261
262 # turn lumas/chromas into np array to ease multi-dimensional plots/asserts
263 lumas = np.array(lumas)
264 chromas = np.array(chromas)
265
266 # create plot of luma & chroma averages vs quality
Clemenz Portmann6816c022021-03-01 13:00:12 -0800267 plot_data(QUALITIES, lumas, chromas, img_name)
Clemenz Portmann813d3962020-03-06 15:15:29 -0800268
269 # assert decreasing luma/chroma with improved jpeg quality
270 for i in range(lumas.shape[1]):
Clemenz Portmann6816c022021-03-01 13:00:12 -0800271 l = lumas[:, i]
272 c = chromas[:, i]
273 if not all(y < x * JPEG_DQT_TOL for x, y in zip(l, l[1:])):
274 raise AssertionError(f'luma DQT avgs: {l}, TOL: {JPEG_DQT_TOL}')
Clemenz Portmann813d3962020-03-06 15:15:29 -0800275
Clemenz Portmann6816c022021-03-01 13:00:12 -0800276 if not all(y < x * JPEG_DQT_TOL for x, y in zip(c, c[1:])):
277 raise AssertionError(f'chroma DQT avgs: {c}, TOL: {JPEG_DQT_TOL}')
Clemenz Portmann813d3962020-03-06 15:15:29 -0800278
279if __name__ == '__main__':
Clemenz Portmann6816c022021-03-01 13:00:12 -0800280 test_runner.main()