Home | History | Annotate | Download | only in dng_noise_model
      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 
     15 import its.device
     16 import its.caps
     17 import its.objects
     18 import its.image
     19 import os.path
     20 import pylab
     21 import matplotlib
     22 import matplotlib.pyplot as plt
     23 import math
     24 import Image
     25 import time
     26 import numpy as np
     27 import scipy.stats
     28 import 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.
     32 def 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 
     38 def 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 
    290 if __name__ == '__main__':
    291     main()
    292 
    293