/** Released under the GPL. Use at your own risk.
*
* @author Albert Cardona, Johannes Schindelin
*/
package ini.trakem2.imaging;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import mpicbg.imglib.algorithm.OutputAlgorithm;
import mpicbg.imglib.container.array.ArrayContainerFactory;
import mpicbg.imglib.cursor.Cursor;
import mpicbg.imglib.cursor.LocalizableByDimCursor;
import mpicbg.imglib.image.Image;
import mpicbg.imglib.image.ImageFactory;
import mpicbg.imglib.type.logic.BitType;
import mpicbg.imglib.type.numeric.integer.IntType;
/** Given two binary images of the same dimensions,
* generate an interpolated image that sits somewhere
* in between, as specified by the weight.
*
* For each binary image, the edges are found
* and then each pixel is assigned a distance to the nearest edge.
* Inside, distance values are positive; outside, negative.
* Then both processed images are compared, and wherever
* the weighted sum is larger than zero, the result image
* gets a pixel set to true (or white, meaning inside).
*
* A weight of zero means that the first image is not present at all
* in the interpolated image;
* a weight of one means that the first image is present exclusively.
*
* The code was originally created by Johannes Schindelin
* in the VIB's vib.BinaryInterpolator class, for ij.ImagePlus.
*/
public class BinaryInterpolation2D implements OutputAlgorithm<BitType>
{
final private Image<BitType> img1, img2;
private float weight;
private Image<BitType> interpolated;
private String errorMessage;
private IDT2D idt1, idt2;
public BinaryInterpolation2D(final Image<BitType> img1, final Image<BitType> img2, final float weight) {
this.img1 = img1;
this.img2 = img2;
this.weight = weight;
}
/** NOT thread safe, stateful. */
private final class IDT2D {
final Image<IntType> result;
final int w, h;
final int[] position = new int[2];
final LocalizableByDimCursor<BitType> csrc;
final LocalizableByDimCursor<IntType> cout;
IDT2D(final Image<BitType> img) {
this.w = img.getDimension(0);
this.h = img.getDimension(1);
ImageFactory<IntType> f = new ImageFactory<IntType>(new IntType(), new ArrayContainerFactory());
this.result = f.createImage(new int[]{w, h});
// Set all result pixels to infinity
final int infinity = (w + h) * 9;
for (final IntType v : this.result) {
v.set(infinity);
}
// init result pixels with those of the image:
this.csrc = img.createLocalizableByDimCursor();
this.cout = result.createLocalizableByDimCursor();
int count = 0;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
if (isBoundary(x, y)) {
setOutValueAt(x, y, 0);
count++;
} else if (isJustOutside(x, y)) {
setOutValueAt(x, y, -1);
}
}
}
if (count > 0) {
propagate();
}
csrc.close();
cout.close();
}
private final void setPosition(final LocalizableByDimCursor<?> c, final int x, final int y) {
position[0] = x;
position[1] = y;
c.setPosition(position);
}
private final void setOutValueAt(final int x, final int y, final int value) {
setPosition(cout, x, y);
cout.getType().set(value);
}
private final int getSrcValueAt(final int x, final int y) {
setPosition(csrc, x, y);
return csrc.getType().get() ? 1 : 0;
}
private final int getOutValueAt(final int x, final int y) {
setPosition(cout, x, y);
return cout.getType().get();
}
// reads from result, writes to result
private final void idt(final int x, final int y, final int dx, final int dy) {
if (x + dx < 0 || y + dy < 0 ||
x + dx >= w || y + dy >= h) {
return;
}
int value = getOutValueAt(x + dx, y + dy);
final int distance = (dx == 0 || dy == 0 ? 3 : 4);
value += distance * (value < 0 ? -1 : 1);
setPosition(cout, x, y);
if (Math.abs(cout.getType().get()) > Math.abs(value)) {
cout.getType().set(value);
}
}
private final void propagate() {
for (int j = 0; j < h; j++)
for (int i = 0; i < w; i++) {
idt(i, j, -1, 0);
idt(i, j, -1, -1);
idt(i, j, 0, -1);
}
for (int j = h - 1; j >= 0; j--)
for (int i = w - 1; i >= 0; i--) {
idt(i, j, +1, 0);
idt(i, j, +1, +1);
idt(i, j, 0, +1);
}
for (int i = w - 1; i >= 0; i--)
for (int j = h - 1; j >= 0; j--) {
idt(i, j, +1, 0);
idt(i, j, +1, +1);
idt(i, j, 0, +1);
}
for (int i = 0; i < w; i++)
for (int j = 0; j < h; j++) {
idt(i, j, -1, 0);
idt(i, j, -1, -1);
idt(i, j, 0, -1);
}
}
private final boolean isBoundary(final int x, final int y) {
if (getSrcValueAt(x, y) == 0)
return false;
if (x <= 0 || getSrcValueAt(x - 1, y) == 0)
return true;
if (x >= w - 1 || getSrcValueAt(x + 1, y) == 0)
return true;
if (y <= 0 || getSrcValueAt(x, y - 1) == 0)
return true;
if (y >= h - 1 || getSrcValueAt(x, y + 1) == 0)
return true;
if (x <= 0 || y <= 0 || getSrcValueAt(x - 1, y - 1) == 0)
return true;
if (x <= 0 || y >= h - 1 || getSrcValueAt(x - 1, y + 1) == 0)
return true;
if (x >= w - 1 || y <= 0 || getSrcValueAt(x + 1, y - 1) == 0)
return true;
if (x >= w - 1 || y >= h - 1 || getSrcValueAt(x + 1, y + 1) == 0)
return true;
return false;
}
private final boolean isJustOutside(final int x, final int y) {
if (getSrcValueAt(x, y) != 0)
return false;
if (x > 0 && getSrcValueAt(x - 1, y) != 0)
return true;
if (x < w - 1 && getSrcValueAt(x + 1, y) != 0)
return true;
if (y > 0 && getSrcValueAt(x, y - 1) != 0)
return true;
if (y < h - 1 && getSrcValueAt(x, y + 1) != 0)
return true;
if (x > 0 && y > 0 && getSrcValueAt(x - 1, y - 1) != 0)
return true;
if (x > 0 && y < h - 1 && getSrcValueAt(x - 1, y + 1) != 0)
return true;
if (x < w - 1 && y > 0 && getSrcValueAt(x + 1, y - 1) != 0)
return true;
if (x < w - 1 && y < h - 1 && getSrcValueAt(x + 1, y + 1) != 0)
return true;
return false;
}
}
@Override
public Image<BitType> getResult() {
return interpolated;
}
@Override
public boolean checkInput() {
if (img1.getNumDimensions() < 2 || img2.getNumDimensions() < 2) {
errorMessage = "Need at least 2 dimensions";
return false;
}
if (img1.getDimension(0) != img2.getDimension(0) || img1.getDimension(1) != img2.getDimension(1)) {
errorMessage = "Dimensions do not match";
return false;
}
if (weight < 0 || weight > 1) {
errorMessage = "Weight must be between 0 and 1, both inclusive.";
return false;
}
return true;
}
@Override
public String getErrorMessage() {
return errorMessage;
}
private final class NewITD2D implements Callable<IDT2D> {
private final Image<BitType> img;
NewITD2D(final Image<BitType> img) {
this.img = img;
}
@Override
public IDT2D call() throws Exception {
return new IDT2D(img);
}
}
/** After changing the weight, it's totally valid to call process() again,
* and then getResult(). */
public void setWeight(final float weight) throws IllegalArgumentException {
if (weight < 0 || weight > 1) {
throw new IllegalArgumentException("Weight must be between 0 and 1, both inclusive.");
}
this.weight = weight;
}
@Override
public boolean process() {
this.interpolated = process(this.weight);
return null != this.interpolated;
}
/** The first time, it will prepare the distance transform images, which are computed only once. */
public Image<BitType> process(final float weight)
{
synchronized (this) {
if (null == idt1 || null == idt2) {
ExecutorService exec = Executors.newFixedThreadPool(Math.min(2, Runtime.getRuntime().availableProcessors()));
Future<IDT2D> fu1 = exec.submit(new NewITD2D(img1));
Future<IDT2D> fu2 = exec.submit(new NewITD2D(img2));
exec.shutdown();
try {
this.idt1 = fu1.get();
this.idt2 = fu2.get();
} catch (InterruptedException ie) {
throw new RuntimeException(ie);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
// Cannot just img1.createNewImage() because the container may not be able to receive data,
// such as the ShapeList container.
final ImageFactory<BitType> f = new ImageFactory<BitType>(new BitType(), new ArrayContainerFactory());
final Image<BitType> interpolated = f.createImage(new int[]{img1.getDimension(0), img1.getDimension(1)});
if (img1.getContainer().compareStorageContainerCompatibility(img2.getContainer())) {
final Cursor<IntType> c1 = idt1.result.createCursor();
final Cursor<IntType> c2 = idt2.result.createCursor();
final Cursor<BitType> ci = interpolated.createCursor();
while (ci.hasNext()) {
c1.fwd();
c2.fwd();
ci.fwd();
if ((c1.getType().get() * weight) + (c2.getType().get() * (1 - weight)) > 0) {
ci.getType().set(true);
}
}
c1.close();
c2.close();
ci.close();
} else {
System.out.println("using option 2");
final LocalizableByDimCursor<IntType> c1 = idt1.result.createLocalizableByDimCursor();
final LocalizableByDimCursor<IntType> c2 = idt2.result.createLocalizableByDimCursor();
final LocalizableByDimCursor<BitType> ci = interpolated.createLocalizableByDimCursor();
while (ci.hasNext()) {
ci.fwd();
c1.setPosition(ci);
c2.setPosition(ci);
if (0 <= c1.getType().get() * weight + c2.getType().get() * (1 - weight)) {
ci.getType().set(true);
}
}
c1.close();
c2.close();
ci.close();
}
return interpolated;
}
}