尽可能快地从PGM或TIFF创建JavaFX图像

时间:2014-06-08 12:13:48

标签: java javafx tiff imagej pgm

我正在使用java从扫描仪设备捕获图像。输入格式为PGM或TIFF。我必须在用户界面中显示实时结果。实际上我使用ImageJ来读取源输入流为tiff,因为ImageJ也可以处理不完整的流。之后,ImagePlus对象将转换为BufferedImage,最后转换为JavaFX Image

ImagePlus imagePlus = new Opener().openTiff(inputStream, "");
BufferedImage bufferedImage = imagePlus.getBufferedImage();
Image image = SwingFXUtils.toFXImage(bufferedImage, null);

这很慢。我需要一种更快的方法来从PGM或TIFF流创建JavaFX Image。似乎JavaFX实际上不支持这种格式,我没有找到有用的库。

有什么想法吗?

编辑#1

我决定分两步优化图像捕捉。首先,在更新ui中的图像时,我需要更好的状态控制。这实际上已经完成并且工作正常。现在,当转换线程忙时,将删除更新请求。第二步是使用自实现的pnm阅读器(基于建议的实现)并逐步更新模型中的图像......直到扫描过程完成。这应该减少从设备加载图像时所需的资源。我需要更改我的架构的某些部分才能实现这一点。

感谢@ all的评论。

btw:java 8 lambdas非常棒:)

编辑#2

由于JavaFX的线程测试,我的计划不起作用:(

目前我的后端有一个WritableImage,应该逐步填充数据。此图像实例设置为ObjectProperty,最终绑定到ImageView。由于WritableImageImageView相关联,因此无法使用PixelWriter填充数据。这会导致异常。

java.lang.IllegalStateException: Not on FX application thread; currentThread = pool-2-thread-1
    at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:210) ~[jfxrt.jar:na]
    at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:393) ~[jfxrt.jar:na]
    at javafx.scene.Scene.addToDirtyList(Scene.java:529) ~[jfxrt.jar:na]
    at javafx.scene.Node.addToSceneDirtyList(Node.java:417) ~[jfxrt.jar:na]
    at javafx.scene.Node.impl_markDirty(Node.java:408) ~[jfxrt.jar:na]
    at javafx.scene.Node.transformedBoundsChanged(Node.java:3789) ~[jfxrt.jar:na]
    at javafx.scene.Node.impl_geomChanged(Node.java:3753) ~[jfxrt.jar:na]
    at javafx.scene.image.ImageView.access$700(ImageView.java:141) ~[jfxrt.jar:na]
    at javafx.scene.image.ImageView$3.invalidated(ImageView.java:285) ~[jfxrt.jar:na]
    at javafx.beans.WeakInvalidationListener.invalidated(WeakInvalidationListener.java:83) ~[jfxrt.jar:na]
    at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:135) ~[jfxrt.jar:na]
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80) ~[jfxrt.jar:na]
    at javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74) ~[jfxrt.jar:na]
    at javafx.scene.image.Image$ObjectPropertyImpl.fireValueChangedEvent(Image.java:568) ~[jfxrt.jar:na]
    at javafx.scene.image.Image.pixelsDirty(Image.java:542) ~[jfxrt.jar:na]
    at javafx.scene.image.WritableImage$2.setArgb(WritableImage.java:170) ~[jfxrt.jar:na]
    at javafx.scene.image.WritableImage$2.setColor(WritableImage.java:179) ~[jfxrt.jar:na]

我的解决方法是创建图像的副本,但我不喜欢这个解决方案。也许可以阻止自动更改通知并手动执行此操作?

2 个答案:

答案 0 :(得分:1)

作为一项实验,为了学习一些JavaFX,我决定亲自了解在上面的评论中实施我所建议的内容有多难......: - )

PGM读数改编自我的PNM ImageIO插件,似乎工作正常。据报道,对于我的640x480样本图像,读取时间约为70-90毫秒(如果有,请随时向我发送更多样本!)。

未压缩的TIFF应该可以在大致相同的时间内读取,尽管TIFF IFD结构比非常简单的PGM标头解析起来更复杂。 TIFF压缩会增加一些解压缩开销,具体取决于压缩类型和设置。

import java.io.DataInputStream;
import java.io.IOException;

import javax.imageio.IIOException;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class PGMTest extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws IOException {
        Label root = new Label();
        Image image;

        long start = System.currentTimeMillis();
        DataInputStream input = new DataInputStream(getClass().getResourceAsStream("/house.l.pgm"));
        try {
            image = readImage(input);
        } finally {
            input.close();
        }
        System.out.printf("Read image (%f x %f) in: %d ms\n", image.getWidth(), image.getHeight(), System.currentTimeMillis() - start);

        root.setGraphic(new ImageView(image));
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Image readImage(final DataInputStream input) throws IOException {
        // First parse PGM header
        PNMHeader header = PNMHeader.parse(input);

        WritableImage image = new WritableImage(header.getWidth(), header.getHeight());
        PixelWriter pixelWriter = image.getPixelWriter();

        int maxSample = header.getMaxSample(); // Needed for normalization

//        PixelFormat<ByteBuffer> gray = PixelFormat.createByteIndexedInstance(createGrayColorMap());

        byte[] rowBuffer = new byte[header.getWidth()];
        for (int y = 0; y < header.getHeight(); y++) {
            input.readFully(rowBuffer); // Read one row

//            normalize(rowBuffer, maxSample);
//            pixelWriter.setPixels(0, y, width, 1, gray, rowBuffer, 0, width); // Gives weird NPE for me...

            // As I can't get setPixels to work, we'll set pixels directly
            // Performance is probably worse than setPixels, but it seems "ok"-ish
            for (int x = 0; x < rowBuffer.length; x++) {
                int gray = (rowBuffer[x] & 0xff) * 255 / maxSample; // Normalize [0...255]
                pixelWriter.setArgb(x, y, 0xff000000 | gray << 16 | gray << 8 | gray);
            }
        }

        return image;
    }

    private int[] createGrayColorMap() {
        int[] colors = new int[256];
        for (int i = 0; i < colors.length; i++) {
            colors[i] = 0xff000000 | i << 16 | i << 8 | i;
        }
        return colors;
    }

    /**
     * Simplified version of my PNMHeader parser
     */
    private static class PNMHeader {
        public static final int PGM = 'P' << 8 | '5';

        private final int width;
        private final int height;
        private final int maxSample;

        private PNMHeader(final int width, final int height, final int maxSample) {
            this.width = width;
            this.height = height;
            this.maxSample = maxSample;
        }

        public int getWidth() {
            return width;
        }

        public int getHeight() {
            return height;
        }

        public int getMaxSample() {
            return maxSample;
        }

        public static PNMHeader parse(final DataInputStream input) throws IOException {
            short type = input.readShort();

            if (type != PGM) {
                throw new IIOException(String.format("Only PGM binay (P5) supported for now: %04x", type));
            }

            int width = 0;
            int height = 0;
            int maxSample = 0;

            while (width == 0 || height == 0 || maxSample == 0) {
                String line = input.readLine(); // For PGM I guess this is ok...

                if (line == null) {
                    throw new IIOException("Unexpeced end of stream");
                }

                if (line.indexOf('#') >= 0) {
                    // Skip comment
                    continue;
                }

                line = line.trim();

                if (!line.isEmpty()) {
                    // We have tokens...
                    String[] tokens = line.split("\\s");
                    for (String token : tokens) {
                        if (width == 0) {
                            width = Integer.parseInt(token);
                        } else if (height == 0) {
                            height = Integer.parseInt(token);
                        } else if (maxSample == 0) {
                            maxSample = Integer.parseInt(token);
                        } else {
                            throw new IIOException("Unknown PBM token: " + token);
                        }
                    }
                }
            }

            return new PNMHeader(width, height, maxSample);
        }
    }
}

我应该补充一点,我使用JavaFX 2.2在Java 7上编写,编译和运行上面的代码。


更新:使用预定义的PixelFormat我能够使用PixelWriter.setPixels,因此对于相同的640x480样本图像,读取时间进一步缩短至45-60毫秒。这是readImage的新版本(代码在其他方面相同):

private Image readImage(final DataInputStream input) throws IOException {
    // First parse PGM header
    PNMHeader header = PNMHeader.parse(input);

    int width = header.getWidth();
    int height = header.getHeight();
    WritableImage image = new WritableImage(width, height);
    PixelWriter pixelWriter = image.getPixelWriter();

    int maxSample = header.getMaxSample(); // Needed to normalize

    PixelFormat<ByteBuffer> format = PixelFormat.getByteRgbInstance();

    byte[] rowBuffer = new byte[width * 3]; // * 3 to hold RGB 
    for (int y = 0; y < height; y++) {
        input.readFully(rowBuffer, 0, width); // Read one row

        // Expand gray to RGB triplets
        for (int i = width - 1; i > 0; i--) {
            byte gray = (byte) ((rowBuffer[i] & 0xff) * 255 / maxSample); // Normalize [0...255];
            rowBuffer[i * 3    ] = gray;
            rowBuffer[i * 3 + 1] = gray;
            rowBuffer[i * 3 + 2] = gray;
        }

        pixelWriter.setPixels(0, y, width, 1, format, rowBuffer, 0, width * 3);
    }

    return image;
}

答案 1 :(得分:1)

下载 jai_imageio.jar 并将其包含在您的项目中。 将tiff图像转换为fx可读图像的代码如下:

String pathToImage = "D:\\ABC.TIF";
ImageInputStream is;
try {
is = ImageIO.createImageInputStream(new File(pathToImage));  //read tiff using imageIO (JAI component)
if (is == null || is.length() == 0) {
    System.out.println("Image is null");
}

Iterator<ImageReader> iterator = ImageIO.getImageReaders(is);
if (iterator == null || !iterator.hasNext()) {
    throw new IOException("Image file format not supported by ImageIO: " + pathToImage);
}
ImageReader reader = (ImageReader) iterator.next();
reader.setInput(is);
int nbPages = reader.getNumImages(true);
BufferedImage bf = reader.read(0);   //1st page of tiff file
BufferedImage bf1 = reader.read(1);  //2nd page of tiff file
WritableImage wr = null;
WritableImage wr1 = null;
if (bf != null) {
    wr= SwingFXUtils.toFXImage(bf, null);   //convert bufferedImage (awt) into Writable Image(fx)
}
if (bf != null) {
    wr1= SwingFXUtils.toFXImage(bf1, null);  //convert bufferedImage (awt) into Writable Image(fx)
}
img_view1.setImage(wr);
img_view2.setImage(wr1);

} catch (FileNotFoundException ex) {
        Logger.getLogger(Image_WindowController.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
        Logger.getLogger(Image_WindowController.class.getName()).log(Level.SEVERE, null, ex);
}

这是我在Stack Overflow上的第一个答案。希望它有所帮助!