/*
* This file is part of the JFeatureLib project: https://github.com/locked-fg/JFeatureLib
* JFeatureLib is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* JFeatureLib is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JFeatureLib; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* You are kindly asked to refer to the papers of the according authors which
* should be mentioned in the Javadocs of the respective classes as well as the
* JFeatureLib project itself.
*
* Hints how to cite the projects can be found at
* https://github.com/locked-fg/JFeatureLib/wiki/Citation
*/
package de.lmu.ifi.dbs.jfeaturelib.features.sift;
import ij.ImagePlus;
import ij.io.FileSaver;
import ij.process.ImageProcessor;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.log4j.Logger;
/**
* Code that Wraps the Sift Executable.
*
* <code>
* SiftWrapper t = new SiftWrapper(new File("c:/temp/siftWin32.exe"));
* List<double[]> features = t.getFeatures(new File("c:\\temp\\ant-sample.pgm"));
* </code>
*
* An image passed to this class is first saved as a .pgm in the systems temp directory. This file is passed
* to the sift binary and deleted afterwards.
*
* @author Franz Graf
*/
public class SiftWrapper {
private static final int MAX_SIFT_TIMEOUT = 5;
private static final int NUMBER_OF_GRADIENTS = 128;
private static final String PREFIX = "JFeatureLib-SiftWrapper";
private static final String SUFFIX = ".pgm";
private static final Logger log = Logger.getLogger(SiftWrapper.class.getName());
private final ProcessBuilder processBuilder;
/**
* creates the sift wrapper
*
* @param siftBinary the SIFT executable
* @throws IOException if exec is not an executable
*/
public SiftWrapper(File siftBinary) throws IOException {
if (!siftBinary.canExecute()) {
throw new IOException("Sift Binary not executable: " + siftBinary);
}
processBuilder = new ProcessBuilder(new String[]{siftBinary.getAbsolutePath()});
processBuilder.redirectErrorStream(true);
}
/**
* Package private constructor for tests
*/
SiftWrapper() {
this.processBuilder = null;
}
public List<double[]> getFeatures(ImageProcessor ip) throws IOException,
InterruptedException {
File tmpFile = File.createTempFile(PREFIX, SUFFIX);
List<double[]> features;
try {
ImagePlus iPlus = new ImagePlus(tmpFile.getAbsolutePath());
iPlus.setProcessor("", ip);
new FileSaver(iPlus).saveAsPgm(tmpFile.getAbsolutePath());
features = getFeatures(tmpFile);
} finally {
tmpFile.delete();
}
return features;
}
/**
* Creates and returnes Sift-Features from the given file. The file MUST be a PGM image.
*
* @param f PGM image
* @return features
* @throws IOException
* @throws InterruptedException
*/
public List<double[]> getFeatures(File f) throws IOException,
InterruptedException {
if (!f.getName().toLowerCase().endsWith(SUFFIX)) {
log.warn("File does not have a .pgm extension. Sure it is a PGM file? " + f.getAbsolutePath());
}
processBuilder.redirectInput(ProcessBuilder.Redirect.from(f));
Process p = processBuilder.start();
String siftOutput = getOutput(p);
p.waitFor(MAX_SIFT_TIMEOUT, TimeUnit.SECONDS);
return siftToArray(extractFeatures(siftOutput));
}
private String getOutput(Process p) throws IOException {
StringBuilder sb = new StringBuilder();
try (BufferedInputStream is = new BufferedInputStream(p.getInputStream())) {
int x;
while ((x = is.read()) != -1) {
sb.append((char) x);
}
}
return sb.toString();
}
/**
* Converts a list of Sift Feature Vectors into a list of plain arrays
*/
private List<double[]> siftToArray(List<SiftFeatureVector> sifts) {
List<double[]> arrays = new ArrayList<>(sifts.size());
for (SiftFeatureVector vector : sifts) {
arrays.add(vector.asArray());
}
return arrays;
}
/**
* Extract features from the streamreader. Siftfeatures all have their primary key = 0 and class = -1
*
* @param isr streamreader from which the input is read
* @return array of {@link SiftFeatureVector}s (may be empty)
* @throws IOException
*/
List<SiftFeatureVector> extractFeatures(String siftOutput)
throws IOException {
final String[] lines = siftOutput.split("\n");
final int features = Integer.parseInt(lines[0].split(" ")[0]);
final List<SiftFeatureVector> vectors = new ArrayList<>(features);
SiftFeatureVector currentVector = null;
List<Double> dataList = new ArrayList<>(NUMBER_OF_GRADIENTS);
boolean allow = true;
for (int linecount = 1; allow && linecount < lines.length; linecount++) { // line 0 is just a comment line
String inLine = lines[linecount];
if (log.isTraceEnabled()) {
log.trace(inLine);
}
// Each NEW vector begins with a non-space-char.
// Vector data then follows with a space at the beginning of
// each line. The end is some comments.
if (inLine.startsWith("Finding")) {
allow = false;
} else if (inLine.charAt(0) != ' ') {
// create new vector and add header info
String[] split = inLine.split(" ");
double y = new Double(split[0]);
double x = new Double(split[1]);
double scale = new Double(split[2]);
double rotation = new Double(split[3]);
currentVector = new SiftFeatureVector(x, y, scale, rotation);
} else {
String[] split = inLine.trim().split(" ");
// check size of resulting vector
if (dataList.size() + split.length > NUMBER_OF_GRADIENTS) {
throw new IllegalArgumentException("adding too much elements to the vector");
}
for (String part : split) {
dataList.add(Double.parseDouble(part));
}
// 128 data elements found. The vector is completed.
// convert array to list and add it to the vector
if (dataList.size() == NUMBER_OF_GRADIENTS) {
double[] dataArr = new double[NUMBER_OF_GRADIENTS];
for (int i = 0; i < dataList.size(); i++) {
dataArr[i] = dataList.get(i);
}
currentVector.setGradients(dataArr);
vectors.add(currentVector);
// reset variables
currentVector = null;
dataList.clear();
}
}
}
return vectors;
}
}