• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 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
15import its.device
16import its.caps
17import its.objects
18import its.image
19import os.path
20import pylab
21import matplotlib
22import matplotlib.pyplot as plt
23import math
24import Image
25import time
26import numpy as np
27import scipy.stats
28import scipy.signal
29
30# Convert a 2D array a to a 4D array with dimensions [tile_size,
31# tile_size, row, col] where row, col are tile indices.
32def tile(a, tile_size):
33    tile_rows, tile_cols = a.shape[0]/tile_size, a.shape[1]/tile_size
34    a = a.reshape([tile_rows, tile_size, tile_cols, tile_size])
35    a = a.transpose([1, 3, 0, 2])
36    return a
37
38def main():
39    """Capture a set of raw images with increasing gains and measure the noise.
40    """
41    NAME = os.path.basename(__file__).split(".")[0]
42
43    # How many sensitivities per stop to sample.
44    steps_per_stop = 2
45    # How large of tiles to use to compute mean/variance.
46    tile_size = 64
47    # Exposure bracketing range in stops
48    bracket_stops = 4
49    # How high to allow the mean of the tiles to go.
50    max_signal_level = 0.5
51    # Colors used for plotting the data for each exposure.
52    colors = 'rygcbm'
53
54    # Define a first order high pass filter to eliminate low frequency
55    # signal content when computing variance.
56    f = np.array([-1, 1]).astype('float32')
57    # Make it a higher order filter by convolving the first order
58    # filter with itself a few times.
59    f = np.convolve(f, f)
60    f = np.convolve(f, f)
61
62    # Compute the normalization of the filter to preserve noise
63    # power. Let a be the normalization factor we're looking for, and
64    # Let X and X' be the random variables representing the noise
65    # before and after filtering, respectively. First, compute
66    # Var[a*X']:
67    #
68    #   Var[a*X'] = a^2*Var[X*f_0 + X*f_1 + ... + X*f_N-1]
69    #             = a^2*(f_0^2*Var[X] + f_1^2*Var[X] + ... + (f_N-1)^2*Var[X])
70    #             = sum(f_i^2)*a^2*Var[X]
71    #
72    # We want Var[a*X'] to be equal to Var[X]:
73    #
74    #    sum(f_i^2)*a^2*Var[X] = Var[X] -> a = sqrt(1/sum(f_i^2))
75    #
76    # We can just bake this normalization factor into the high pass
77    # filter kernel.
78    f = f/math.sqrt(np.dot(f, f))
79
80    bracket_factor = math.pow(2, bracket_stops)
81
82    with its.device.ItsSession() as cam:
83        props = cam.get_camera_properties()
84
85        # Get basic properties we need.
86        sens_min, sens_max = props['android.sensor.info.sensitivityRange']
87        sens_max_analog = props['android.sensor.maxAnalogSensitivity']
88        white_level = props['android.sensor.info.whiteLevel']
89
90        print "Sensitivity range: [%f, %f]" % (sens_min, sens_max)
91        print "Max analog sensitivity: %f" % (sens_max_analog)
92
93        # Do AE to get a rough idea of where we are.
94        s_ae,e_ae,_,_,_  = \
95            cam.do_3a(get_results=True, do_awb=False, do_af=False)
96        # Underexpose to get more data for low signal levels.
97        auto_e = s_ae*e_ae/bracket_factor
98
99        # If the auto-exposure result is too bright for the highest
100        # sensitivity or too dark for the lowest sensitivity, report
101        # an error.
102        min_exposure_ns, max_exposure_ns = \
103            props['android.sensor.info.exposureTimeRange']
104        if auto_e < min_exposure_ns*sens_max:
105            raise its.error.Error("Scene is too bright to properly expose \
106                                  at the highest sensitivity")
107        if auto_e*bracket_factor > max_exposure_ns*sens_min:
108            raise its.error.Error("Scene is too dark to properly expose \
109                                  at the lowest sensitivity")
110
111        # Start the sensitivities at the minimum.
112        s = sens_min
113
114        samples = []
115        plots = []
116        measured_models = []
117        while s <= sens_max + 1:
118            print "ISO %d" % round(s)
119            fig = plt.figure()
120            plt_s = fig.gca()
121            plt_s.set_title("ISO %d" % round(s))
122            plt_s.set_xlabel("Mean signal level")
123            plt_s.set_ylabel("Variance")
124
125            samples_s = []
126            for b in range(0, bracket_stops + 1):
127                # Get the exposure for this sensitivity and exposure time.
128                e = int(math.pow(2, b)*auto_e/float(s))
129                req = its.objects.manual_capture_request(round(s), e)
130                cap = cam.do_capture(req, cam.CAP_RAW)
131                planes = its.image.convert_capture_to_planes(cap, props)
132
133                samples_e = []
134                for (pidx, p) in enumerate(planes):
135                    p = p.squeeze()
136
137                    # Crop the plane to be a multiple of the tile size.
138                    p = p[0:p.shape[0] - p.shape[0]%tile_size,
139                          0:p.shape[1] - p.shape[1]%tile_size]
140
141                    # convert_capture_to_planes normalizes the range
142                    # to [0, 1], but without subtracting the black
143                    # level.
144                    black_level = its.image.get_black_level(
145                        pidx, props, cap["metadata"])
146                    p = p*white_level
147                    p = (p - black_level)/(white_level - black_level)
148
149                    # Use our high pass filter to filter this plane.
150                    hp = scipy.signal.sepfir2d(p, f, f).astype('float32')
151
152                    means_tiled = \
153                        np.mean(tile(p, tile_size), axis=(0, 1)).flatten()
154                    vars_tiled = \
155                        np.var(tile(hp, tile_size), axis=(0, 1)).flatten()
156
157                    for (mean, var) in zip(means_tiled, vars_tiled):
158                        # Don't include the tile if it has samples that might
159                        # be clipped.
160                        if mean + 2*math.sqrt(var) < max_signal_level:
161                            samples_e.append([mean, var])
162
163                    means_e, vars_e = zip(*samples_e)
164                    plt_s.plot(means_e, vars_e, colors[b%len(colors)] + ',')
165
166                    samples_s.extend(samples_e)
167
168            [S, O, R, p, stderr] = scipy.stats.linregress(samples_s)
169            measured_models.append([round(s), S, O])
170            print "Sensitivity %d: %e*y + %e (R=%f)" % (round(s), S, O, R)
171
172            # Add the samples for this sensitivity to the global samples list.
173            samples.extend([(round(s), mean, var) for (mean, var) in samples_s])
174
175            # Add the linear fit to the plot for this sensitivity.
176            plt_s.plot([0, max_signal_level], [O, O + S*max_signal_level], 'r-',
177                       label="Linear fit")
178            xmax = max([x for (x, _) in samples_s])*1.25
179            plt_s.set_xlim(xmin=0, xmax=xmax)
180            plt_s.set_ylim(ymin=0, ymax=(O + S*xmax)*1.25)
181            fig.savefig("%s_samples_iso%04d.png" % (NAME, round(s)))
182            plots.append([round(s), fig])
183
184            # Move to the next sensitivity.
185            s = s*math.pow(2, 1.0/steps_per_stop)
186
187        # Grab the sensitivities and line parameters from each sensitivity.
188        S_measured = [e[1] for e in measured_models]
189        O_measured = [e[2] for e in measured_models]
190        sens = np.asarray([e[0] for e in measured_models])
191        sens_sq = np.square(sens)
192
193        # Use a global linear optimization to fit the noise model.
194        gains = np.asarray([s[0] for s in samples])
195        means = np.asarray([s[1] for s in samples])
196        vars_ = np.asarray([s[2] for s in samples])
197
198        # Define digital gain as the gain above the max analog gain
199        # per the Camera2 spec. Also, define a corresponding C
200        # expression snippet to use in the generated model code.
201        digital_gains = np.maximum(gains/sens_max_analog, 1)
202        digital_gain_cdef = "(sens / %d.0) < 1.0 ? 1.0 : (sens / %d.0)" % \
203            (sens_max_analog, sens_max_analog)
204
205        # Find the noise model parameters via least squares fit.
206        ad = gains*means
207        bd = means
208        cd = gains*gains
209        dd = digital_gains*digital_gains
210        a = np.asarray([ad, bd, cd, dd]).T
211        b = vars_
212
213        # To avoid overfitting to high ISOs (high variances), divide the system
214        # by the gains.
215        a = a/(np.tile(gains, (a.shape[1], 1)).T)
216        b = b/gains
217
218        [A, B, C, D], _, _, _ = np.linalg.lstsq(a, b)
219
220        # Plot the noise model components with the values predicted by the
221        # noise model.
222        S_model = A*sens + B
223        O_model = \
224            C*sens_sq + D*np.square(np.maximum(sens/sens_max_analog, 1))
225
226        (fig, (plt_S, plt_O)) = plt.subplots(2, 1)
227        plt_S.set_title("Noise model")
228        plt_S.set_ylabel("S")
229        plt_S.loglog(sens, S_measured, 'r+', basex=10, basey=10,
230                     label="Measured")
231        plt_S.loglog(sens, S_model, 'bx', basex=10, basey=10, label="Model")
232        plt_S.legend(loc=2)
233
234        plt_O.set_xlabel("ISO")
235        plt_O.set_ylabel("O")
236        plt_O.loglog(sens, O_measured, 'r+', basex=10, basey=10,
237                     label="Measured")
238        plt_O.loglog(sens, O_model, 'bx', basex=10, basey=10, label="Model")
239        fig.savefig("%s.png" % (NAME))
240
241        for [s, fig] in plots:
242            plt_s = fig.gca()
243
244            dg = max(s/sens_max_analog, 1)
245            S = A*s + B
246            O = C*s*s + D*dg*dg
247            plt_s.plot([0, max_signal_level], [O, O + S*max_signal_level], 'b-',
248                       label="Model")
249            plt_s.legend(loc=2)
250
251            plt.figure(fig.number)
252
253            # Re-save the plot with the global model.
254            fig.savefig("%s_samples_iso%04d.png" % (NAME, round(s)))
255
256        # Generate the noise model implementation.
257        print """
258        /* Generated test code to dump a table of data for external validation
259         * of the noise model parameters.
260         */
261        #include <stdio.h>
262        #include <assert.h>
263        double compute_noise_model_entry_S(int sens);
264        double compute_noise_model_entry_O(int sens);
265        int main(void) {
266            int sens;
267            for (sens = %d; sens <= %d; sens += 100) {
268                double o = compute_noise_model_entry_O(sens);
269                double s = compute_noise_model_entry_S(sens);
270                printf("%%d,%%lf,%%lf\\n", sens, o, s);
271            }
272            return 0;
273        }
274
275        /* Generated functions to map a given sensitivity to the O and S noise
276         * model parameters in the DNG noise model.
277         */
278        double compute_noise_model_entry_S(int sens) {
279            double s = %e * sens + %e;
280            return s < 0.0 ? 0.0 : s;
281        }
282
283        double compute_noise_model_entry_O(int sens) {
284            double digital_gain = %s;
285            double o = %e * sens * sens + %e * digital_gain * digital_gain;
286            return o < 0.0 ? 0.0 : o;
287        }
288        """ % (sens_min, sens_max, A, B, digital_gain_cdef, C, D)
289
290if __name__ == '__main__':
291    main()
292
293