blob: cc117be4c53eda9f1d615a439e2199ecad040648 [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
13
14# limitations under the License.
15
16import math
17import os.path
18
19import its.caps
20import its.device
21import its.image
22import its.objects
23
24from matplotlib import pylab
25import matplotlib.pyplot
26import numpy as np
27
28JPEG_APPN_MARKERS = [[255, 224], [255, 225], [255, 226], [255, 227], [255, 228],
Clemenz Portmanna6a17452021-01-07 09:43:24 -080029 [255, 229], [255, 230], [255, 231], [255, 232], [255, 235]]
Clemenz Portmann813d3962020-03-06 15:15:29 -080030JPEG_DHT_MARKER = [255, 196] # JPEG Define Huffman Table
31JPEG_DQT_MARKER = [255, 219] # JPEG Define Quantization Table
32JPEG_DQT_TOL = 0.8 # -20% for each +20 in jpeg.quality (empirical number)
33JPEG_EOI_MARKER = [255, 217] # JPEG End of Image
34JPEG_SOI_MARKER = [255, 216] # JPEG Start of Image
35JPEG_SOS_MARKER = [255, 218] # JPEG Start of Scan
36NAME = os.path.basename(__file__).split('.')[0]
37QUALITIES = [25, 45, 65, 85]
38SYMBOLS = ['o', 's', 'v', '^', '<', '>']
39
40
41def is_square(integer):
42 root = math.sqrt(integer)
43 return integer == int(root + 0.5) ** 2
44
45
46def strip_soi_marker(jpeg):
47 """strip off start of image marker.
48
49 SOI is of form [xFF xD8] and JPEG needs to start with marker.
50
51 Args:
52 jpeg: 1-D numpy int [0:255] array; values from JPEG capture
53
54 Returns:
55 jpeg with SOI marker stripped off.
56 """
57
58 soi = jpeg[0:2]
59 assert list(soi) == JPEG_SOI_MARKER, 'JPEG has no Start Of Image marker'
60 return jpeg[2:]
61
62
63def strip_appn_data(jpeg):
64 """strip off application specific data at beginning of JPEG.
65
66 APPN markers are of form [xFF, xE*, size_msb, size_lsb] and should follow
67 SOI marker.
68
69 Args:
70 jpeg: 1-D numpy int [0:255] array; values from JPEG capture
71
72 Returns:
73 jpeg with APPN marker(s) and data stripped off.
74 """
75
76 length = 0
77 i = 0
78 # find APPN markers and strip off payloads at beginning of jpeg
79 while i < len(jpeg)-1:
80 if [jpeg[i], jpeg[i+1]] in JPEG_APPN_MARKERS:
81 length = jpeg[i+2] * 256 + jpeg[i+3] + 2
82 print ' stripped APPN length:', length
83 jpeg = np.concatenate((jpeg[0:i], jpeg[length:]), axis=None)
84 elif ([jpeg[i], jpeg[i+1]] == JPEG_DQT_MARKER or
85 [jpeg[i], jpeg[i+1]] == JPEG_DHT_MARKER):
86 break
87 else:
88 i += 1
89
90 return jpeg
91
92
93def find_dqt_markers(marker, jpeg):
94 """Find location(s) of marker list in jpeg.
95
96 DQT marker is of form [xFF, xDB].
97
98 Args:
99 marker: list; marker values
100 jpeg: 1-D numpy int [0:255] array; JPEG capture w/ SOI & APPN stripped
101
102 Returns:
103 locs: list; marker locations in jpeg
104 """
105 locs = []
106 marker_len = len(marker)
107 for i in xrange(len(jpeg)-marker_len+1):
108 if list(jpeg[i:i+marker_len]) == marker:
109 locs.append(i)
110 return locs
111
112
113def extract_dqts(jpeg, debug=False):
114 """Find and extract the DQT info in the JPEG.
115
116 SOI marker and APPN markers plus data are stripped off front of JPEG.
117 DQT marker is of form [xFF, xDB] followed by [size_msb, size_lsb].
118 Size includes the size values, but not the marker values.
119 Luma DQT is prefixed by 0, Chroma DQT by 1.
120 DQTs can have both luma & chroma or each individually.
121 There can be more than one DQT table for luma and chroma.
122
123 Args:
124 jpeg: 1-D numpy int [0:255] array; values from JPEG capture
125 debug: bool; command line flag to print debug data
126
127 Returns:
128 lumas, chromas: lists of numpy means of luma & chroma DQT matrices.
129 Higher values represent higher compression.
130 """
131
132 dqt_markers = find_dqt_markers(JPEG_DQT_MARKER, jpeg)
133 print 'DQT header loc(s):', dqt_markers
134 lumas = []
135 chromas = []
136 for i, dqt in enumerate(dqt_markers):
137 if debug:
138 print '\n DQT %d start: %d, marker: %s, length: %s' % (
139 i, dqt, jpeg[dqt:dqt+2], jpeg[dqt+2:dqt+4])
140 dqt_size = jpeg[dqt+2]*256 + jpeg[dqt+3] - 2 # strip off size marker
141 if dqt_size % 2 == 0: # even payload means luma & chroma
142 print ' both luma & chroma DQT matrices in marker'
143 dqt_size = (dqt_size - 2) / 2 # subtact off luma/chroma markers
144 assert is_square(dqt_size), 'DQT size: %d' % dqt_size
145 luma_start = dqt + 5 # skip header, length, & matrix id
146 chroma_start = luma_start + dqt_size + 1 # skip lumen & matrix_id
147 luma = np.array(jpeg[luma_start:luma_start+dqt_size])
148 chroma = np.array(jpeg[chroma_start:chroma_start+dqt_size])
149 lumas.append(np.mean(luma))
150 chromas.append(np.mean(chroma))
151 if debug:
152 h = int(math.sqrt(dqt_size))
153 print ' luma:', luma.reshape(h, h)
154 print ' chroma:', chroma.reshape(h, h)
155 else: # odd payload means only 1 matrix
156 print ' single DQT matrix in marker'
157 dqt_size = dqt_size - 1 # subtract off luma/chroma marker
158 assert is_square(dqt_size), 'DQT size: %d' % dqt_size
159 start = dqt + 5
160 matrix = np.array(jpeg[start:start+dqt_size])
161 if jpeg[dqt+4]: # chroma == 1
162 chromas.append(np.mean(matrix))
163 if debug:
164 h = int(math.sqrt(dqt_size))
165 print ' chroma:', matrix.reshape(h, h)
166 else: # luma == 0
167 lumas.append(np.mean(matrix))
168 if debug:
169 h = int(math.sqrt(dqt_size))
170 print ' luma:', matrix.reshape(h, h)
171
172 return lumas, chromas
173
174
175def plot_data(qualities, lumas, chromas):
176 """Create plot of data."""
177 print 'qualities: %s' % str(qualities)
178 print 'luma DQT avgs: %s' % str(lumas)
179 print 'chroma DQT avgs: %s' % str(chromas)
180 pylab.title(NAME)
181 for i in range(lumas.shape[1]):
182 pylab.plot(qualities, lumas[:, i], '-g'+SYMBOLS[i],
183 label='luma_dqt'+str(i))
184 pylab.plot(qualities, chromas[:, i], '-r'+SYMBOLS[i],
185 label='chroma_dqt'+str(i))
186 pylab.xlim([0, 100])
187 pylab.ylim([0, None])
188 pylab.xlabel('jpeg.quality')
189 pylab.ylabel('DQT luma/chroma matrix averages')
190 pylab.legend(loc='upper right', numpoints=1, fancybox=True)
191 matplotlib.pyplot.savefig('%s_plot.png' % NAME)
192
193
194def main():
195 """Test the camera JPEG compression quality.
196
197 Step JPEG qualities through android.jpeg.quality. Ensure quanitization
198 matrix decreases with quality increase. Matrix should decrease as the
199 matrix represents the division factor. Higher numbers --> fewer quantization
200 levels.
201 """
202
203 # determine debug
204 debug = its.caps.debug_mode()
205
206 # init variables
207 lumas = []
208 chromas = []
209
210 with its.device.ItsSession() as cam:
211 props = cam.get_camera_properties()
Clemenz Portmann454b64e2020-04-09 12:48:29 -0700212 its.caps.skip_unless(its.caps.jpeg_quality(props))
Clemenz Portmann813d3962020-03-06 15:15:29 -0800213 cam.do_3a()
214
215 # do captures over jpeg quality range
216 req = its.objects.auto_capture_request()
217 for q in QUALITIES:
218 print '\njpeg.quality: %.d' % q
219 req['android.jpeg.quality'] = q
220 cap = cam.do_capture(req, cam.CAP_JPEG)
221 jpeg = cap['data']
222
223 # strip off start of image
224 jpeg = strip_soi_marker(jpeg)
225
226 # strip off application specific data
227 jpeg = strip_appn_data(jpeg)
228 print 'remaining JPEG header:', jpeg[0:4]
229
230 # find and extract DQTs
231 lumas_i, chromas_i = extract_dqts(jpeg, debug)
232 lumas.append(lumas_i)
233 chromas.append(chromas_i)
234
235 # save JPEG image
236 img = its.image.convert_capture_to_rgb_image(cap, props=props)
237 its.image.write_image(img, '%s_%d.jpg' % (NAME, q))
238
239 # turn lumas/chromas into np array to ease multi-dimensional plots/asserts
240 lumas = np.array(lumas)
241 chromas = np.array(chromas)
242
243 # create plot of luma & chroma averages vs quality
244 plot_data(QUALITIES, lumas, chromas)
245
246 # assert decreasing luma/chroma with improved jpeg quality
247 for i in range(lumas.shape[1]):
248 l = lumas[:, i]
249 c = chromas[:, i]
250 emsg = 'luma DQT avgs: %s, TOL: %.1f' % (str(l), JPEG_DQT_TOL)
251 assert all(y < x * JPEG_DQT_TOL for x, y in zip(l, l[1:])), emsg
252 emsg = 'chroma DQT avgs: %s, TOL: %.1f' % (str(c), JPEG_DQT_TOL)
253 assert all(y < x * JPEG_DQT_TOL for x, y in zip(c, c[1:])), emsg
254
255
256if __name__ == '__main__':
257 main()