View Javadoc
1   package com.github.celldynamics.quimp.plugin.dic;
2   
3   import org.slf4j.Logger;
4   import org.slf4j.LoggerFactory;
5   
6   import com.github.celldynamics.quimp.plugin.utils.ImageProcessorPlus;
7   
8   import ij.ImagePlus;
9   import ij.process.FloatProcessor;
10  import ij.process.ImageProcessor;
11  import ij.process.ImageStatistics;
12  
13  /**
14   * Implementation of Line Integration and Deconvolution algorithm proposed by Kam.
15   * 
16   * <p><h1>Principles</h1>
17   * This algorithm uses corrected line integration that runs in both directions from current point of
18   * image (<i>x_n,y_n</i>) to <i>r_1</i> and end <i>r_2</i>. Vector <i>dr</i> is set to be parallel
19   * to 0X axis. Final formula for reconstruction of pixel (<i>x_n,y_n</i>) is given by:<br>
20   * <img src="doc-files/eq.png"/><br>
21   * 
22   * <p><h1>Algorithm</h1>
23   * The algorithm perform the following steps to reconstruct the whole image:
24   * <ol>
25   * <li>Input is converted to 16bit and stored in private field ExtraImageProcessor
26   * srcImageCopyProcessor
27   * <li>The value of shift is added to all pixels of input image. All operations are performed on
28   * copy of input ImageProcessor or ImagePlus. Rotate image by angle to bring bas-reliefs
29   * perpendicular to OX axis. Rotated image has different dimensions and it is padded by 0. After
30   * this operation 0 pixels are those that belong to background. For every line in rotated image
31   * position of true pixels is calculated. True pixels are those that belong to original image
32   * excluding background added during rotation. For every line position of first and last true pixel
33   * is noted in table ranges
34   * <li>Decay factors are pre-calculated and stored in decays table.
35   * <li>Final reconstruction is performed.
36   * </ol>
37   * Image for reconstruction is passed during construction of DICReconstruction object. For this
38   * object ranges and decays are evaluated and then user can call reconstructionDicLid() method to
39   * get reconstruction. getRanges() method also rotates private object thus rotation in
40   * reconstructionDicLid() is not necessary (flagged by DICReconstruction::isRotated). Different
41   * situation happens when the whole stack is reconstructed. To prevent creating new instance of
42   * DICReconstruction for every slice the setIp(ImageProcessor) method is used for connecting new
43   * slice. In this case it is assumed that ImageProcessor objects are similar and they have the same
44   * geometry. ranges are filled only once on DICReconstruction constructing thus images connected by
45   * setIp(ImageProcessor) are not rotated. This situation is detected in
46   * {@link #reconstructionDicLid()} by <tt>isRotated</tt> flag.
47   * 
48   * <p>Privates:
49   * <ul>
50   * <li><i>ranges</i> - true pixels begin and end on x axis. Set by getRanges(). [r][0] - x of first
51   * pixel of line r of image, [r][1] - x of last pixel of image of line r
52   * <li><i>maxWidth</i> - Set by getRanges()
53   * <li><i>decays</i> - set by generateDeacy(double, int)
54   * <li><i>srcImageCopyProcessor</i> - local <b>copy</b> of input ImageProcessor passed to object.
55   * Set by constructors and setIp(ImageProcessor)
56   * <li><i>isRotated</i> - It is set by getRanges() that rotates object to get true pixels position
57   * and cancelled by setIP
58   * </ul>
59   * 
60   * <p><b>Warning</b>
61   * Currently this class supports only 8bit images. It can support also 16bit input but in this case
62   * algorithm used for detection of true pixels may not work correctly for certain cases - when
63   * maximum intensity will be max-shift
64   * 
65   * <p>The input image can be prefitlered before processing. This is running mean filter of given
66   * mask
67   * size applied at angle perpendicular to the shear (this angle is given by caller).
68   * 
69   * <p><h1>References</h1>
70   * <ul>
71   * <li>Z. Kam, "Microscopic differential interference contrast image processing by line integration
72   * (LID) and deconvolution," Bioimaging, vol. 6, no. 4, pp. 166–176, 1998.
73   * <li>B. Heise, A. Sonnleitner, and E. P. Klement, "DIC image reconstruction on large cell scans.,"
74   * Microsc. Res. Tech., vol. 66, no. 6, pp. 312–320, 2005.
75   * </ul>
76   * 
77   * @author p.baniukiewicz
78   *
79   */
80  public class LidReconstructor {
81  
82    /**
83     * The Constant LOGGER.
84     */
85    static final Logger LOGGER = LoggerFactory.getLogger(LidReconstructor.class.getName());
86    private final int shift = 1; // shift added to original image eliminate 0s
87  
88    private ImageProcessor srcIp; // local reference of ImageProcessor (const)
89    private double decay;
90    private double angle;
91    private double[] decays; // reference to preallocated decay data created
92    private int maxWidth; // Width of image after rotation. Set by getRanges()
93    private int[][] ranges; // true pixels begin and end on x axis.
94    private ImageProcessor srcImageCopyProcessor; // local copy of input
95    private boolean isRotated; // true if srcImageCopyProcessor is rotated
96    private ImageStatistics is;
97    private ImageProcessorPlus ipp; // helper class for rotating images
98    private String prefilterangle;
99    private int masksize;
100 
101   /**
102    * Default constructor that accepts ImagePlus. It does not support stacks.
103    * 
104    * <p>Input srcImage is not modified
105    * 
106    * @param srcImage Image to process
107    * @param decay algorithm constant
108    * @param angle DIC angle
109    * 
110    * @throws DicException Throws exception after generateRanges()
111    */
112   public LidReconstructor(final ImagePlus srcImage, double decay, double angle)
113           throws DicException {
114     this(srcImage.getProcessor(), decay, angle);
115   }
116 
117   /**
118    * Default constructor that accepts ImageProcessor.
119    * 
120    * <p>Input ip is not modified
121    * 
122    * @param ip ImageProcessor to process
123    * @param decay algorithm constant
124    * @param angle DIC angle
125    * 
126    * @throws DicException Throws exception after generateRanges()
127    */
128   public LidReconstructor(final ImageProcessor ip, double decay, double angle) throws DicException {
129     this(ip, decay, angle, "0", 0);
130   }
131 
132   /**
133    * Default constructor.
134    * 
135    * @param ip image to process
136    * @param decay algorithm constant
137    * @param angle DIC angle
138    * @param prefilterangle Supported angle of prefiltering
139    * @param masksize uneven mask size, 0 switches off filtering
140    * @throws DicException wrong image
141    */
142   public LidReconstructor(final ImageProcessor ip, double decay, double angle,
143           String prefilterangle, int masksize) throws DicException {
144     LOGGER.trace("Use params: ip:" + ip + " decay:" + decay + " angle:" + angle + " filterangle:"
145             + prefilterangle + " masksize:" + masksize);
146     this.angle = angle;
147     this.decay = decay;
148     this.isRotated = false;
149     this.prefilterangle = prefilterangle;
150     this.masksize = masksize;
151     ipp = new ImageProcessorPlus();
152     setIp(ip);
153     recalculate();
154   }
155 
156   /**
157    * Sets new reconstruction parameters for current object.
158    * 
159    * @param decay algorithm constant
160    * @param angle DIC angle
161    * @throws DicException Throws exception after generateRanges()
162    */
163   public void setParams(double decay, double angle) throws DicException {
164     this.angle = angle;
165     this.decay = decay;
166     recalculate();
167   }
168 
169   /**
170    * Assigns ImageProcessor for reconstruction to current object. Releases previous one.
171    * 
172    * <p>This method can be used for changing image connected to DICReconstruction object. New image
173    * should have the same architecture as image passed in constructor. Typically this method is
174    * used for passing next slice from stack.
175    * 
176    * <p>Input ip is not modified
177    * 
178    * @param ip New ImageProcessor containing image for reconstruction.
179    */
180   public void setIp(final ImageProcessor ip) {
181     this.srcIp = ip;
182     // make copy of original image to not modify it - converting to 16bit
183     this.srcImageCopyProcessor = srcIp.convertToShort(true);
184     new ImageProcessorPlus().runningMean(srcImageCopyProcessor, prefilterangle, masksize);
185     // ensure that minmax will be recalculated (usually they are stored in class field) set
186     // interpolation
187     srcImageCopyProcessor.resetMinAndMax();
188     srcImageCopyProcessor.setInterpolationMethod(ImageProcessor.BICUBIC);
189     // Rotating image - set 0 background
190     srcImageCopyProcessor.setBackgroundValue(0.0);
191     // getting mean value
192     is = srcImageCopyProcessor.getStatistics();
193     this.isRotated = false; // new Processor not rotated yet
194   }
195 
196   /**
197    * Recalculates true pixels range and new size of image after rotation. Setup private class
198    * fields.
199    * 
200    * <p>Modifies private class fields: <tt>maxWidth</tt>, <tt>ranges</tt>,
201    * <tt>srcImageCopyProcessor</tt>
202    * 
203    * <p><tt>maxWidth</tt> holds width of image after rotation
204    * 
205    * <p><tt>ranges</tt> table holds first and last x position of image line (first and last pixel of
206    * image on background after rotation)
207    * 
208    * <p><tt>srcImageCopyProcessor</tt> is rotated and shifted.
209    * 
210    * @throws DicException when input image is close to saturation e.g. has values of 65536-shift.
211    *         This is due to applied algorithm of detection image pixels after rotation.
212    * @see #reconstructionDicLid()
213    */
214   private void getRanges() throws DicException {
215     double maxpixel; // minimal pixel value
216     int lastpixel; // first and last pixel of image in line
217     int firstpixel;
218     // check condition for removing 0 value from image
219     maxpixel = srcImageCopyProcessor.getMax();
220     if (maxpixel > 65535 - shift) {
221       LOGGER.error("Possible image clipping - check if image is saturated");
222       throw new DicException(String.format(
223               "Possible image clipping - input image has at leas one" + " pixel with value %d",
224               65535 - shift));
225     }
226     // scale pixels by adding 1 - we remove any 0 value from source image
227     srcImageCopyProcessor.add(shift);
228     srcImageCopyProcessor.resetMinAndMax();
229     // rotate image with extending it. borders have the same value as
230     // background
231     srcImageCopyProcessor = ipp.rotate(srcImageCopyProcessor, angle, true);
232     isRotated = true; // current object was rotated
233     // get references of covered object for optimisation purposes
234     int newWidth = srcImageCopyProcessor.getWidth();
235     int newHeight = srcImageCopyProcessor.getHeight();
236     // set private fields - size of image after rotation
237     maxWidth = newWidth;
238     ranges = new int[newHeight][2];
239     // get true pixels positions for every row
240     for (int r = 0; r < newHeight; r++) {
241       // to not process whole line, detect where starts and ends pixels of
242       // image (reject background added during rotation)
243       for (firstpixel = 0; firstpixel < newWidth
244               && srcImageCopyProcessor.get(firstpixel, r) == 0; firstpixel++) {
245         ;
246       }
247       for (lastpixel = newWidth - 1; lastpixel >= 0
248               && srcImageCopyProcessor.get(lastpixel, r) == 0; lastpixel--) {
249         ;
250       }
251       // if empty row, reject it all. reconstructionDicLid process pixels in ranges. such
252       // borders reject processing at all
253       if (firstpixel >= newWidth) {
254         firstpixel = 1;
255         lastpixel = 0;
256       }
257       ranges[r][0] = firstpixel;
258       ranges[r][1] = lastpixel;
259     }
260   }
261 
262   /**
263    * Recalculates tables on demand. Calculates new ranges for true pixels and new decay table.
264    * 
265    * @throws DicException Throws exception after generateRanges()
266    */
267   private void recalculate() throws DicException {
268     // calculate preallocated decay data
269     // generateRanges() must be called first as it initializes fields used
270     // by generateDecay()
271     getRanges();
272     generateDeacy(decay, maxWidth);
273   }
274 
275   /**
276    * Reconstruct DIC image by LID method. This is main method used to reconstruct passed ip
277    * object.
278    * 
279    * <p>The reconstruction algorithm assumes that input image bas-reliefs are oriented horizontally,
280    * thus correct angle should be provided.
281    * 
282    * <p><b>Warning</b>
283    * 
284    * <p>Used optimization with detecting of image pixels based on their value may not be accurate
285    * when input image will be 16-bit and it will contain saturated pixels
286    * 
287    * @return Return reconstruction of srcImage as 16-bit image
288    */
289   public ImageProcessor reconstructionDicLid() {
290     double cumsumup;
291     double cumsumdown;
292     int c;
293     int u;
294     int d;
295     int r; // loop indexes
296     int linindex = 0; // output table linear index
297     if (!isRotated) { // rotate if not rotated in getRanges
298       srcImageCopyProcessor.add(shift); // we use different IP so shift must be added
299       srcImageCopyProcessor = ipp.rotate(srcImageCopyProcessor, angle, true);
300     }
301     // dereferencing for optimization purposes
302     int newWidth = srcImageCopyProcessor.getWidth();
303     int newHeight = srcImageCopyProcessor.getHeight();
304     // create array for storing results - 32bit float as imageprocessor
305     ImageProcessor outputArrayProcessor = new FloatProcessor(newWidth, newHeight);
306     float[] outputPixelArray = (float[]) outputArrayProcessor.getPixels();
307 
308     // do for every row - bas-relief is oriented horizontally
309     for (r = 0; r < newHeight; r++) {
310       // ranges[r][0] - first image pixel in line r
311       // ranges[r][1] - last image pixel in line r
312       linindex = linindex + ranges[r][0];
313       // for every point apply KAM formula
314       for (c = ranges[r][0]; c <= ranges[r][1]; c++) {
315         // up
316         cumsumup = 0;
317         for (u = c; u >= ranges[r][0]; u--) {
318           cumsumup += (srcImageCopyProcessor.get(u, r) - shift - is.mean) * decays[Math.abs(u - c)];
319         }
320         // down
321         cumsumdown = 0; // cumulative sum from point r to the end of column
322         for (d = c; d <= ranges[r][1]; d++) {
323           cumsumdown +=
324                   (srcImageCopyProcessor.get(d, r) - shift - is.mean) * decays[Math.abs(d - c)];
325         }
326         // integral
327         outputPixelArray[linindex] = (float) (cumsumup - cumsumdown);
328         linindex++;
329       }
330       linindex = linindex + newWidth - ranges[r][1] - 1;
331     }
332     // rotate back output processor
333     outputArrayProcessor.setBackgroundValue(0.0);
334     outputArrayProcessor.rotate(-angle);
335     // crop it back to original size
336     outputArrayProcessor =
337             ipp.cropImageAfterRotation(outputArrayProcessor, srcIp.getWidth(), srcIp.getHeight());
338 
339     return outputArrayProcessor.convertToShort(true); // return reconstruction
340   }
341 
342   /**
343    * Generates decay table with exponential distances between pixels multiplied by decay
344    * coefficient.
345    * 
346    * @param decay The value of decay coefficient
347    * @param length Length of table, usually equals to longest processed line on image
348    */
349   private void generateDeacy(double decay, int length) {
350     decays = new double[length];
351     for (int i = 0; i < length; i++) {
352       decays[i] = Math.exp(-decay * i);
353     }
354   }
355 
356 }