• 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
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],
29                     [255, 235]]
30JPEG_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()
212        its.caps.skip_unless(its.caps.jpeg_quality(props))
213        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()
258