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