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 }