Можно ли читать несколько изображений из InputStream с помощью Java ImageIO?

Я пытаюсь создать поток Kotlin, который просто считывает несколько изображений из одного файла InputStream.

Для тестирования у меня есть входной поток, который получает содержимое двух небольших файлов изображений в отдельном потоке. Кажется, это работает правильно, так как если я записываю содержимое этого входного потока на диск, результирующий файл идентичен конкатенации двух исходных файлов изображений.

Проблема возникает при чтении изображений из входного потока с помощью ImageIO:

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
import javax.imageio.ImageIO;

class ImgReader {

    InputStream input;

    ImgReader(InputStream input) {
        this.input = input;
    }

    public void run() {
        ImageIO.setUseCache(false);
        System.out.println("read start");
        int counter = 1;
        try {
            BufferedImage im = ImageIO.read(input);
            System.out.println("read: " + counter + " " + (im != null));

            if (im != null)
                ImageIO.write(im, "jpg", new File("pics/out/" + (counter++) +".jpeg"));

        } catch (Exception e){
            System.out.println("error while reading stream");
            e.printStackTrace(System.out);
        }

        System.out.println("read done");
    }
}

Это работает для первого изображения, которое правильно получено и сохранено в файл. Однако второе изображение не читается: ImageIO.read(input) возвращает null.

Можно ли прочитать несколько изображений из InputStream? Что я делаю неправильно?

--- РЕДАКТИРОВАТЬ ---

Пробовал вариант, где из потока декодируется только одно изображение (это сделано правильно). После этого я попытался сохранить остальную часть содержимого потока в двоичный файл, не пытаясь декодировать его как изображение. Этот второй двоичный файл пуст, а это означает, что первый ImageIO.read, кажется, потребляет весь поток.


person Eloy Villasclaras    schedule 26.11.2018    source источник
comment
Хотя ImageIO.read не закрывает InputStream, он не обязательно располагается в начале следующего изображения в конце.   -  person Maurice Perry    schedule 26.11.2018
comment
Не могли бы вы переписать свой пример на Java. Проблема не в Котлине, и наличие этого кода на Java позволяет большему количеству людей понять его и помочь вам.   -  person talex    schedule 26.11.2018
comment
Вам нужно каким-то образом получить байты, которые были прочитаны ImageIO, и пропустить их в скопированном InputStream.   -  person Minn    schedule 26.11.2018
comment
Если я вас правильно понял, вы записали два изображения в один и тот же файл (одно за другим, объединенные), а затем попытались прочитать его обратно, как будто эти файлы были сохранены как отдельные файлы? Есть ли Kotlin Lib, которая поддерживает чтение таких объединенных изображений? Возможно, было бы лучше, если бы у вас было немного времени, чтобы создать проект poc github, чтобы дать нам лучшее представление.   -  person m4gic    schedule 26.11.2018
comment
Я думаю, что Морис Перри нашел проблему. Я попробовал вариант, в котором я читал только одно изображение из потока, а затем сохранял остальные в двоичный файл (не пытаясь декодировать изображение). Изображение сохранилось корректно, но ImageIO.read поглотил весь поток, поэтому второй бинарный файл пуст.   -  person Eloy Villasclaras    schedule 26.11.2018
comment
@ m4gic, нет, я объединил их в один файл, чтобы убедиться, что я правильно записываю их из отдельных файлов в поток. Я надеялся, что ImageIO сможет это сделать, но, видимо, он работает только с одним изображением на поток.   -  person Eloy Villasclaras    schedule 26.11.2018
comment
Возможно, это могло бы работать и для большего количества изображений, но я боюсь, что вам придется поддерживать границы изображений в вашем потоке (я имею в виду, что вы можете прочитать его, если знаете начальную и конечную позицию...)   -  person m4gic    schedule 26.11.2018
comment
m4gic, я сейчас изучаю это. У меня не будет контроля над потоком, так как я делаю это, чтобы иметь возможность читать таймлапс из raspistill без сохранения изображений на диск (просто читать их из стандартного вывода). Я попытаюсь узнать, сколько байтов потока занимает каждое изображение, прочитав поток. Затем я могу сохранить каждый байт изображения в отдельный массив и использовать его с ImageIO.read. (Я думаю, что raspistill использует EXIF).   -  person Eloy Villasclaras    schedule 26.11.2018
comment
Строго говоря, проблема здесь не в ImageIO: с некоторыми изменениями можно увидеть, что здесь виноват лежащий в основе ImageReader. Например, JPEGImageReader действительно читает весь поток, в то время как PNGImageReader читает (примерно) только те данные, которые необходимы для определения (первого) изображения. Поскольку нет способа предотвратить это от ImageReader и нет способа определить, использовались ли уже байты из ввода для изображения, я боюсь, что это невозможно. Интересный вопрос, однако, +1   -  person Marco13    schedule 26.11.2018


Ответы (3)


Да, можно прочитать несколько изображений из (одного) InputStream.

Я считаю, что наиболее очевидным решением является использование формата файла, который уже широко поддерживает несколько изображений, например TIFF. javax.imageio API имеет хорошую поддержку для чтения и записи файлов с несколькими изображениями, даже несмотря на то, что класс ImageIO не имеет для него никаких удобных методов, таких как методы ImageIO.read(...)/ImageIO.write(...) для чтения/записи одного изображения. Это означает, что вам нужно написать немного больше кода (примеры кода ниже).

Однако, если ввод создан третьей стороной, не зависящей от вас, использование другого формата может оказаться невозможным. Из комментариев объясняется, что ваш ввод на самом деле представляет собой поток объединенных файлов Exif JPEG. Хорошей новостью является то, что JPEGImageReader/Writer в Java позволяет использовать несколько файлов JPEG в одном потоке, хотя это не очень распространенный формат.

Чтобы прочитать несколько файлов JPEG из одного потока, вы можете использовать следующий пример (обратите внимание, что код является полностью универсальным и будет работать для чтения других файлов с несколькими изображениями, таких как TIFF):

File file = ...; // May also use InputStream here
List<BufferedImage> images = new ArrayList<>();

try (ImageInputStream in = ImageIO.createImageInputStream(file)) {
    Iterator<ImageReader> readers = ImageIO.getImageReaders(in);

    if (!readers.hasNext()) {
        throw new AssertionError("No reader for file " + file);
    }

    ImageReader reader = readers.next();

    reader.setInput(in);

    // It's possible to use reader.getNumImages(true) and a for-loop here.
    // However, for many formats, it is more efficient to just read until there's no more images in the stream.
    try {
        int i = 0;
        while (true) {
            images.add(reader.read(i++));
        }
    }
    catch (IndexOutOfBoundsException expected) {
        // We're done
    }

    reader.dispose();
}   

Все, что находится ниже этой строки, является дополнительной дополнительной информацией.

Вот как записывать файлы с несколькими изображениями с помощью ImageIO API (в примере кода используется TIFF, но он достаточно общий и теоретически должен работать и для других форматов, за исключением параметра типа сжатия).

File file = ...; // May also use OutputStream/InputStream here
List<BufferedImage> images = new ArrayList<>(); // Just add images...

Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("TIFF");

if (!writers.hasNext()) {
    throw new AssertionError("Missing plugin");
}

ImageWriter writer = writers.next();

if (!writer.canWriteSequence()) {
    throw new AssertionError("Plugin doesn't support multi page file");       
}

ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionType("JPEG"); // The allowed compression types may vary from plugin to plugin
// The most common values for TIFF, are NONE, LZW, Deflate or Zip, or JPEG

try (ImageOutputStream out = ImageIO.createImageOutputStream(file)) {
    writer.setOutput(out);

    writer.prepareWriteSequence(null); // No stream metadata needed for TIFF

    for (BufferedImage image : images) {
        writer.writeToSequence(new IIOImage(image, null, null), param);
    }

    writer.endWriteSequence();
}

writer.dispose();

Обратите внимание, что до Java 9 вам также понадобится сторонний плагин TIFF, такой как JAI или мой собственный TwelveMonkeys ImageIO, для чтения/записи TIFF с использованием ImageIO.


Другой вариант, если вам действительно не нравится писать этот многословный код, состоит в том, чтобы обернуть изображения в ваш собственный формат минимального контейнера, который включает (как минимум) длину каждого изображения. Затем вы можете писать, используя ImageIO.write(...), и читать, используя ImageIO.read(...), но вам нужно реализовать простую потоковую логику вокруг этого. И главный аргумент против, конечно же, в том, что она будет полностью проприетарной.

Но если вы асинхронно читаете/пишете в клиент-серверной настройке (как я подозреваю из вашего вопроса), это может иметь смысл и может быть приемлемым компромиссом.

Что-то типа:

File file = new File(args[0]);
List<BufferedImage> images = new ArrayList<>();

try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
    ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024 * 1024); // Use larger buffer for large images

    for (BufferedImage image : images) {
        buffer.reset();

        ImageIO.write(image, "JPEG", buffer); // Or PNG or any other format you like, really

        out.writeInt(buffer.size());
        buffer.writeTo(out);
        out.flush();
    }

    out.writeInt(-1); // EOF marker (alternatively, catch EOFException while reading)
}

// And, reading back:
try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
    int size;

    while ((size = in.readInt()) != -1) {
        byte[] buffer = new byte[size];
        in.readFully(buffer); // May be more efficient to create a FilterInputStream that counts bytes read, with local EOF after size

        images.add(ImageIO.read(new ByteArrayInputStream(buffer)));
    }
}

PS: Если все, что вы хотите сделать, это записать полученные образы на диск, вам не следует использовать для этого ImageIO. Вместо этого используйте простой ввод-вывод (предполагая формат из предыдущего примера):

try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
    int counter = 0;

    int size;        
    while ((size = in.readInt()) != -1) {
        byte[] buffer = new byte[size];
        in.readFully(buffer);

        try (FileOutputStream out = new FileOutputStream(new File("pics/out/" + (counter++) +".jpeg"))) {
            out.write(buffer);
            out.flush();
        }
    }
}
person Harald K    schedule 27.11.2018
comment
Спасибо за ответ. Таким образом, чтение нескольких изображений из одного потока работает для файлов TIFF. Я предполагаю, что файл TIFF с несколькими изображениями имеет заголовки, в основном то же самое, что вы делаете в последнем примере (где вы включаете длину файла в поток). На мой взгляд, мой вопрос был немного другим (возможно, он недостаточно хорошо объяснен), поскольку у меня есть входной поток, который получает отдельные файлы изображений (не один TIFF), и которым я не могу манипулировать (чтобы включить длину изображения). - person Eloy Villasclaras; 27.11.2018
comment
@EloyVillasclaras Хорошо... Итак, это немного усложняет задачу. Вы знаете формат этих изображений? Они всегда JPEG? Или всегда PNG? Или случайно? - person Harald K; 27.11.2018
comment
Я пытаюсь прочитать поток stdout из таймлапса raspistill. Изображения EXIF/JPG. Таким образом, проблема заключается в том, что программа чтения JPG использует весь поток для декодирования первого изображения: S. - person Eloy Villasclaras; 27.11.2018
comment
Извините, какой фрагмент вашего кода теперь должен работать с изображениями JPG? Ты имеешь в виду последний? - person Eloy Villasclaras; 28.11.2018
comment
После того, как я переписал ответ, самый первый пример кода будет читать несколько файлов JPEG из одного файла. - person Harald K; 28.11.2018
comment
Я могу подтвердить, что он работает и с входным потоком от raspistill timelapse. В вашем коде вы используете ImageIO.createImageInputStream(file), но он также работает с ImageIO.createImageInputStream(inputStream). Причина, по которой это работает, заключается в том, что с вашим кодом ImageIO создает либо MemoryCacheImageInputStream, либо FileCacheImageInputStream, что позволяет читателю вернуться к следующему изображению. - person Eloy Villasclaras; 28.11.2018
comment
Я расширил свой ответ примером, показывающим, что этот подход действительно работает в некоторых случаях. Приемлемы ли ограничения, зависит от пользователя. - person Marco13; 28.11.2018
comment
@Marco13 Marco13 Он будет работать только с файлами JPEG или настоящими многостраничными файлами TIFF или GIF и т. д. (но не с составными файлами в целом). Однако мы знаем, что OP будет иметь только JPEG. - person Harald K; 28.11.2018

Это хорошо известная «особенность» входных потоков.

Входной поток может быть прочитан только один раз (хорошо, есть mark() и reset(), но не каждая реализация поддерживает это (отметьте markSupported() в Javadoc), и ИМХО это не так удобно использовать), вы должны либо сохранять ваше изображение и передать путь в качестве аргумента, или вы должны прочитать его в массив байтов и создать ByteArrayInputStream для каждого вызова, где вы пытаетесь его прочитать:

// read your original stream once (e.g. with commons IO, just the sake of shortness)
byte[] imageByteArray = IOUtils.toByteArray(input);
...
// and create new input stream every time
InputStream newInput = new ByteArrayInputStream(imageByteArray);
...
// and call your reader in this way:
new ImgReader(newInput);
person m4gic    schedule 26.11.2018
comment
Затем вы продолжаете читать одно и то же изображение, вопрос заключался в том, можно ли читать несколько изображений из одного потока. - person Minn; 26.11.2018
comment
Это была вторая часть его проблемы :) - person m4gic; 26.11.2018
comment
Я не могу сохранить весь поток в массив, потому что это удаляет возможность чтения первого изображения до того, как появится второе (второе может быть задержано) и так далее. - person Eloy Villasclaras; 26.11.2018

Обновлять:

Прокрутите вниз до последнего фрагмента кода, чтобы узнать об обновлении этого ответа.

Это не удовлетворительный ответ, а ответ на вопрос:

Нет, это (почти наверняка) невозможно.

При передаче InputStream в ImageIO он будет внутри заключен в ImageInputStream. Затем этот поток передается в ImageReader. Точная реализация будет зависеть от типа данных изображения. (Обычно это определяется по «магическому заголовку», то есть по первым байтам входных данных).

Теперь поведение этих ImageReader реализаций нельзя изменить или разумно контролировать. (Для некоторых из них фактическое чтение происходит даже в native методах).

Ниже приведен пример, показывающий различные варианты поведения:

  • Во-первых, он генерирует входной поток, содержащий одно изображение JPG и одно изображение PNG. Вывод показывает, что входной поток считывается полностью до того, как будет возвращено изображение JPG.

  • Затем он генерирует входной поток, содержащий одно изображение PNG и одно изображение JPG. Можно видеть, что он читает только несколько байтов, пока не сможет декодировать результат первого изображения PNG.

_

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.imageio.ImageIO;

public class MultipleImagesFromSingleStream
{
    public static void main(String[] args) throws IOException
    {
        readJpgAndPng();
        readPngAndJpg();
    }

    private static void readJpgAndPng() throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(createDummyImage("Image 0", 50), "jpg", baos);
        ImageIO.write(createDummyImage("Image 1", 60), "png", baos);
        byte data[] = baos.toByteArray();
        InputStream inputStream = createSlowInputStream(data);

        BufferedImage image0 = ImageIO.read(inputStream);
        System.out.println("Read " + image0);
        BufferedImage image1 = ImageIO.read(inputStream);
        System.out.println("Read " + image1);
    }

    private static void readPngAndJpg() throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(createDummyImage("Image 0", 50), "png", baos);
        ImageIO.write(createDummyImage("Image 1", 60), "jpg", baos);
        byte data[] = baos.toByteArray();
        InputStream inputStream = createSlowInputStream(data);

        BufferedImage image0 = ImageIO.read(inputStream);
        System.out.println("Read " + image0);
        BufferedImage image1 = ImageIO.read(inputStream);
        System.out.println("Read " + image1);
    }

    private static InputStream createSlowInputStream(byte data[])
    {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        return new InputStream()
        {
            private long counter = 0;
            @Override
            public int read() throws IOException
            {
                counter++;
                if (counter % 100 == 0)
                {
                    System.out.println(
                        "Read " + counter + " of " + data.length + " bytes");
                    try
                    {
                        Thread.sleep(50);
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
                return bais.read();
            }
        };
    }

    private static BufferedImage createDummyImage(String text, int h)
    {
        int w = 100;
        BufferedImage image = 
            new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, w, h);
        g.setColor(Color.WHITE);
        g.drawString(text, 20, 20);
        g.dispose();
        return image;
    }
}

Результат выглядит следующим образом:

Read 100 of 1519 bytes
Read 200 of 1519 bytes
Read 300 of 1519 bytes
Read 400 of 1519 bytes
Read 500 of 1519 bytes
Read 600 of 1519 bytes
Read 700 of 1519 bytes
Read 800 of 1519 bytes
Read 900 of 1519 bytes
Read 1000 of 1519 bytes
Read 1100 of 1519 bytes
Read 1200 of 1519 bytes
Read 1300 of 1519 bytes
Read 1400 of 1519 bytes
Read 1500 of 1519 bytes
Read BufferedImage@3eb07fd3: type = 0 DirectColorModel: rmask=ff000000 gmask=ff0000 bmask=ff00 amask=ff IntegerInterleavedRaster: width = 100 height = 50 #Bands = 4 xOff = 0 yOff = 0 dataOffset[0] 0
Read null
Read 100 of 1499 bytes
Read 200 of 1499 bytes
Read BufferedImage@42110406: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@531d72ca transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 100 height = 50 #numDataElements 4 dataOff[0] = 3
Read null

Обратите внимание, что хотя во втором случае он не считывает весь поток, это все равно не обязательно означает, что входной поток находится в «начале данных JPG». Это всего лишь означает, что он не читает полный поток!

Я также попытался погрузиться в это глубже. Если можно быть уверенным, что изображения всегда являются только изображениями PNG, можно попытаться вручную создать экземпляр PNGImageReader и подключиться к его процессу чтения, чтобы проверить, когда он действительно закончил первое изображение. Но опять же, входной поток внутренне оборачивается в несколько других (буферизованных и дефляционных) входных потоков, и нет никакого способа разумно определить, был ли уже определенный набор байтов «использован» для изображения.

Поэтому я думаю, что единственное разумное решение здесь — закрыть поток после того, как изображение будет прочитано, и открыть новый поток для следующего изображения.


Обходной путь, который обсуждался в комментариях, заключается в добавлении информации о длине в поток. Это означает, что производитель данных изображения сначала записывает в поток int, описывающий длину данных изображения. Затем он записывает данные byte[length] с фактическими данными изображения.

Затем приемник может использовать эту информацию для загрузки отдельных изображений.

Это реализовано здесь, как пример:

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class MultipleImagesFromSingleStreamWorkaround
{
    public static void main(String[] args) throws IOException
    {
        workaround();
    }

    private static void workaround() throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        write(createDummyImage("Image 0", 50), "jpg", baos);
        write(createDummyImage("Image 1", 60), "png", baos);
        write(createDummyImage("Image 2", 70), "gif", baos);
        byte data[] = baos.toByteArray();
        InputStream inputStream = createSlowInputStream(data);

        BufferedImage image0 = read(inputStream);
        System.out.println("Read " + image0);
        BufferedImage image1 = read(inputStream);
        System.out.println("Read " + image1);
        BufferedImage image2 = read(inputStream);
        System.out.println("Read " + image2);

        showImages(image0, image1, image2);
    }

    private static void write(BufferedImage bufferedImage, 
        String formatName, OutputStream outputStream) throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(bufferedImage, formatName, baos);
        byte data[] = baos.toByteArray();
        DataOutputStream dos = new DataOutputStream(outputStream);
        dos.writeInt(data.length);
        dos.write(data);
        dos.flush();
    }

    private static BufferedImage read(
        InputStream inputStream) throws IOException
    {
        DataInputStream dis = new DataInputStream(inputStream);
        int length = dis.readInt();
        byte data[] = new byte[length];
        dis.read(data);
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        return ImageIO.read(bais);
    }




    private static InputStream createSlowInputStream(byte data[])
    {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        return new InputStream()
        {
            private long counter = 0;
            @Override
            public int read() throws IOException
            {
                counter++;
                if (counter % 100 == 0)
                {
                    System.out.println(
                        "Read " + counter + " of " + data.length + " bytes");
                    try
                    {
                        Thread.sleep(50);
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
                return bais.read();
            }
        };
    }

    private static BufferedImage createDummyImage(String text, int h)
    {
        int w = 100;
        BufferedImage image = 
            new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, w, h);
        g.setColor(Color.WHITE);
        g.drawString(text, 20, 20);
        g.dispose();
        return image;
    }


    private static void showImages(BufferedImage ... images)
    {
        SwingUtilities.invokeLater(() -> 
        {
            JFrame f = new JFrame();
            f.getContentPane().setLayout(new GridLayout(1,0));
            for (BufferedImage image : images)
            {
                f.getContentPane().add(new JLabel(new ImageIcon(image)));
            }
            f.pack();
            f.setLocationRelativeTo(null);
            f.setVisible(true);
        });
    }
}

Обновлять

Это основано на ответе haraldK (проголосуйте за его ответ, а не за этот!)

Пример реализации, показывающий подход, предложенный haraldK. Ему удается прочитать последовательность изображений, хотя есть некоторые ограничения:

  • Кажется, что ему нужно прочитать «больше» байтов, чем это строго необходимо, прежде чем он доставит первое изображение.
  • Он не может загружать различные типы изображений (т. е. он не может читать последовательность смешанных изображений PNG и JPG).
  • В частности, мне показалось, что это работает только для изображений JPG. Для PNG или GIF читалось только первое изображение (по крайней мере, для меня...)

Тем не менее, разместив его здесь, чтобы другие могли легко его протестировать:

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;


public class MultipleImagesFromSingleStreamWorking
{
    public static void main(String[] args) throws IOException
    {
        readExample();
    }

    private static void readExample() throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(createDummyImage("Image 0", 50), "jpg", baos);
        //ImageIO.write(createDummyImage("Image 1", 60), "png", baos);
        ImageIO.write(createDummyImage("Image 2", 70), "jpg", baos);
        ImageIO.write(createDummyImage("Image 3", 80), "jpg", baos);
        ImageIO.write(createDummyImage("Image 4", 90), "jpg", baos);
        ImageIO.write(createDummyImage("Image 5", 100), "jpg", baos);
        ImageIO.write(createDummyImage("Image 6", 110), "jpg", baos);
        ImageIO.write(createDummyImage("Image 7", 120), "jpg", baos);
        byte data[] = baos.toByteArray();
        InputStream inputStream = createSlowInputStream(data);

        List<BufferedImage> images = readImages(inputStream);
        showImages(images);
    }

    private static List<BufferedImage> readImages(InputStream inputStream)
        throws IOException
    {
        // From https://stackoverflow.com/a/53501316/3182664
        List<BufferedImage> images = new ArrayList<BufferedImage>();
        try (ImageInputStream in = ImageIO.createImageInputStream(inputStream))
        {
            Iterator<ImageReader> readers = ImageIO.getImageReaders(in);

            if (!readers.hasNext())
            {
                throw new AssertionError("No reader for file " + inputStream);
            }

            ImageReader reader = readers.next();

            reader.setInput(in);

            // It's possible to use reader.getNumImages(true) and a for-loop
            // here.
            // However, for many formats, it is more efficient to just read
            // until there's no more images in the stream.
            try
            {
                int i = 0;
                while (true)
                {
                    BufferedImage image = reader.read(i++);
                    System.out.println("Read " + image);
                    images.add(image);
                }
            }
            catch (IndexOutOfBoundsException expected)
            {
                // We're done
            }

            reader.dispose();
        }
        return images;
    }

    private static InputStream createSlowInputStream(byte data[])
    {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        return new InputStream()
        {
            private long counter = 0;
            @Override
            public int read() throws IOException
            {
                counter++;
                if (counter % 100 == 0)
                {
                    System.out.println(
                        "Read " + counter + " of " + data.length + " bytes");
                    try
                    {
                        Thread.sleep(50);
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
                return bais.read();
            }
        };
    }

    private static BufferedImage createDummyImage(String text, int h)
    {
        int w = 100;
        BufferedImage image = 
            new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, w, h);
        g.setColor(Color.WHITE);
        g.drawString(text, 20, 20);
        g.dispose();
        return image;
    }


    private static void showImages(List<BufferedImage> images)
    {
        SwingUtilities.invokeLater(() -> 
        {
            JFrame f = new JFrame();
            f.getContentPane().setLayout(new GridLayout(1,0));
            for (BufferedImage image : images)
            {
                f.getContentPane().add(new JLabel(new ImageIcon(image)));
            }
            f.pack();
            f.setLocationRelativeTo(null);
            f.setVisible(true);
        });
    }
}
person Marco13    schedule 26.11.2018
comment
Я нашел тот же результат для JPG. Другой взгляд на это заключается в том, что протокол связи, в котором изображения добавляются к потоку без каких-либо метаданных, на самом деле не имеет смысла. Чтобы на самом деле добиться этого, источник должен добавить некоторые метаданные. Простая потоковая передача целого числа (или длинного), содержащего длину изображения до фактических данных изображения, позволит отправлять несколько изображений по потоку. - person Eloy Villasclaras; 27.11.2018
comment
Я думаю, что это правильный (хотя и немного разочаровывающий) ответ, поэтому я отмечу его как принятый. Если бы позже было опубликовано рабочее решение, я бы изменил принятый ответ. - person Eloy Villasclaras; 27.11.2018
comment
@EloyVillasclaras Конечно! Отправка данных в форме length(int) + data[length] позволит вам прочитать длину на принимающей стороне, затем просто прочитать length байта в массив и передать это ImageIO для чтения изображения. Это позволит держать поток открытым. Это также должно быть довольно просто. Может быть, я смогу выделить время, чтобы расширить тест с помощью этого подхода к решению. - person Marco13; 27.11.2018
comment
@ Marco13 Marco13 Думаю, это хорошее объяснение того, почему код в вопросе не работает. Но я не согласен с выводом, поэтому я добавил свой собственный ответ. :-) - person Harald K; 27.11.2018
comment
@haraldK Конечно, если можно прибегнуть к контейнеру с несколькими изображениями, такому как TIFF, это будет вариант. Использование length+data[length], которое мы обсуждали и о котором вы также упомянули, кажется мне более жизнеспособным. Но, возможно, ограничения вопроса (еще) не отсортированы. Посмотрим, каковы будут результаты обсуждения вашего комментария. - person Marco13; 27.11.2018
comment
@ Marco13 Marco13 Обновленный ответ от haraldK действительно работает, в том числе с выходным потоком из raspistill timelapse без каких-либо изменений. Поэтому я отмечу его ответ как правильный. Я надеюсь, что это нормально. - person Eloy Villasclaras; 28.11.2018
comment
@ Marco13 Marco13, как я уже упоминал в другом комментарии, причина, по которой принятый код ответа работает, заключается в том, что с этой реализацией ImageIO создает либо MemoryCacheImageInputStream, либо FileCacheImageInputStream обертку исходного потока. Таким образом, даже если программа чтения JPEG потребляет больше байтов, чем необходимо для чтения изображения, кэшированный поток можно найти обратно. - person Eloy Villasclaras; 28.11.2018
comment
@EloyVillasclaras Я добавил еще одно обновление, но этот подход, похоже, также имеет некоторые ограничения ... - person Marco13; 28.11.2018