Использование ImageMagick для эффективного сшивания изображения линейного сканирования

Я ищу альтернативы для камер с линейным сканированием, которые будут использоваться в спортивных хронометражах, или, скорее, в той части, где нужно определить местоположение. Я обнаружил, что обычные промышленные камеры могут легко соответствовать скорости коммерческих решений камеры при>1000 кадров в секунду. Для моих потребностей, как правило, точность синхронизации не важна, но относительное размещение спортсменов. Я подумал, что мог бы использовать для этой цели одну из самых дешевых промышленных камер Basler, IDS или любую другую индустриальную камеру. Конечно, существуют камеры с линейным сканированием, которые могут работать со скоростью, превышающей несколько тысяч кадров в секунду (или Гц), но есть возможность приобрести камеры с зонным сканированием, которые могут выполнять требуемые 1000-3000 кадров в секунду менее чем за 500€.

Моим святым Граалем, конечно же, будут возможности композиции изображений FinishLynx (или любой другой системы линейного сканирования) почти в реальном времени, в основном эта часть: https://youtu.be/7CWZvFcwSEk?t=23s

Весь процесс, который я думал для моей альтернативы:

  • Используйте Basler Pylon Viewer (или другое программное обеспечение) для записи изображений шириной 2 пикселя с самой высокой скоростью чтения камеры. Для камеры, которую я сейчас использую, это означает, что она должна быть повернута на бок, а высота должна быть уменьшена, поскольку это единственный способ считывать кадры 1920x2px @ >250 кадров в секунду.
  • Создайте программу или пакетный сценарий, который затем соединяет эти кадры 1920x2px, например, в одну секунду записи кадров 1000*1920x2px, что означает результирующее изображение с разрешением 1920x2000px (по горизонтали x по вертикали).
  • Наконец, используя ту же программу или иным способом, просто поверните изображение, чтобы оно отражало положение камеры, и, таким образом, получайте изображение с разрешением 2000x1920 пикселей (снова по горизонтали и по вертикали)
  • Откройте изображение в программе анализа (в настоящее время ImageJ), чтобы быстро проанализировать результаты

Я не программист, но это то, что я смог собрать, просто используя пакетные сценарии, конечно, с помощью stackru.

  • В настоящее время запись целых 10 секунд, например, на диск в виде потока raw/mjpeg(avi/mkv), может выполняться в режиме реального времени.
  • Запись отдельных кадров в формате TIFF или BMP или использование FFMPEG для их сохранения в формате PNG или JPG занимает ~20-60 секунд. Затем добавление и поворот занимает еще ~45-60 секунд. Все это должно быть достигнуто менее чем за 60 секунд в течение 10 секунды съемки (1000-3000 кадров в секунду при 10 с = 10000-30000 кадров), поэтому мне нужно что-то быстрее.

Мне удалось выяснить, как быть довольно эффективным с ImageMagick:

magick convert -limit file 16384 -limit memory 8GiB -interlace Plane -quality 85 -append +rotate 270 “%folder%\Basler*.Tiff” “%out%”

#%out% has a .jpg -filename that is dynamically made from folder name and number of frames.

Эта команда работает и возвращает мне 10000 кадров, кодированных примерно за 30 секунд на i5-2520m (хотя большая часть обработки, похоже, использует только один поток, так как она работает с использованием 25% процессора). Это полученное изображение: https://i.imgur.com/OD4RqL7.jpg (19686x1928px)

Однако, поскольку запись в кадры TIFF с использованием Basler Pylon Viewer занимает намного больше времени, чем запись видеопотока MJPEG, я хотел бы использовать файл MJPEG (avi/mkv) в качестве источника для добавления. Я заметил, что FFMPEG имеет команду "image2pipe", которая должна иметь возможность напрямую передавать изображения в ImageMagick. Я не смог заставить это работать, хотя:

   $ ffmpeg.exe -threads 4 -y -i "Basler acA1920-155uc (21644989)_20180930_043754312.avi" -f image2pipe - | convert - -interlace Plane -quality 85 -append +rotate 270 "%out%" >> log.txt
    ffmpeg version 3.4 Copyright (c) 2000-2017 the FFmpeg developers
      built with gcc 7.2.0 (GCC)
      configuration: –enable-gpl –enable-version3 –enable-sdl2 –enable-bzlib –enable-fontconfig –enable-gnutls –enable-iconv –enable-libass –enable-libbluray –enable-libfreetype –enable-libmp3lame –enable-libopenjpeg –enable-libopus –enable-libshine –enable-libsnappy –enable-libsoxr –enable-libtheora –enable-libtwolame –enable-libvpx –enable-libwavpack –enable-libwebp –enable-libx264 –enable-libx265 –enable-libxml2 –enable-libzimg –enable-lzma –enable-zlib –enable-gmp –enable-libvidstab –enable-libvorbis –enable-cuda –enable-cuvid –enable-d3d11va –enable-nvenc –enable-dxva2 –enable-avisynth –enable-libmfx
      libavutil      55. 78.100 / 55. 78.100
      libavcodec     57.107.100 / 57.107.100
      libavformat    57. 83.100 / 57. 83.100
      libavdevice    57. 10.100 / 57. 10.100
      libavfilter     6.107.100 /  6.107.100
      libswscale      4.  8.100 /  4.  8.100
      libswresample   2.  9.100 /  2.  9.100
      libpostproc    54.  7.100 / 54.  7.100
    Invalid Parameter - -interlace
    [mjpeg @ 000000000046b0a0] EOI missing, emulating
    Input #0, avi, from 'Basler acA1920-155uc (21644989)_20180930_043754312.avi’:
      Duration: 00:00:50.02, start: 0.000000, bitrate: 1356 kb/s
        Stream #0:0: Video: mjpeg (MJPG / 0x47504A4D), yuvj422p(pc, bt470bg/unknown/unknown), 1920x2, 1318 kb/s, 200 fps, 200 tbr, 200 tbn, 200 tbc
    Stream mapping:
      Stream #0:0 -> #0:0 (mjpeg (native) -> mjpeg (native))
    Press [q] to stop, [?] for help
    Output #0, image2pipe, to ‘pipe:’:
      Metadata:
        encoder         : Lavf57.83.100
        Stream #0:0: Video: mjpeg, yuvj422p(pc), 1920x2, q=2-31, 200 kb/s, 200 fps, 200 tbn, 200 tbc
        Metadata:
          encoder         : Lavc57.107.100 mjpeg
        Side data:
          cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: -1
    av_interleaved_write_frame(): Invalid argument
    Error writing trailer of pipe:: Invalid argument
    frame=    1 fps=0.0 q=1.6 Lsize=       0kB time=00:00:00.01 bitrate= 358.4kbits/s speed=0.625x
    video:0kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000000%
    Conversion failed!

Если я поднимусь немного выше по высоте, у меня больше не будет ошибки "[mjpeg @ 000000000046b0a0] EOI отсутствует, эмуляция". Однако все это будет работать только с кадрами <2px high/wide.

редактировать: да, я также могу использовать ffmpeg -i file.mpg -r 1/1 $filename%03d.bmp или же ffmpeg -i file.mpg $filename%03d.bmp извлечь все кадры из потока MJPEG/RAW. Однако это дополнительный шаг, который я не хочу делать. (просто удаление папки из 30000 jpgs занимает 2 минуты в одиночку...)

Может кто-нибудь придумать рабочее решение для метода трубопровода или совершенно другой альтернативный способ обработки этого?

2 ответа

Решение

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

Вместо того, чтобы передать 2x1920 строк развертки в ImageMagick для его объединения и записи в формате JPEG, я сделал следующее:

  • создал полный выходной кадр заранее в программе на C++, а затем зацикливал чтение в строке развертки 2x1920 на каждой итерации и вставлял это в правильную позицию в выходном кадре, и

  • когда вся последовательность будет прочитана, сожмите ее в JPEG с помощью turbo-jpeg и запишите на диск.

Таким образом, ImageMagick больше не требуется. Вся программа теперь запускается за 1,3 секунды, а не через 10,3 секунды с помощью ImageMagick.

Вот код:

////////////////////////////////////////////////////////////////////////////////
// stitch.cpp
// Mark Setchell
//
// Read 2x1920 RGB frames from `ffmpeg` and stitch into 20000x1920 RGB image.
////////////////////////////////////////////////////////////////////////////////
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <unistd.h>
#include <turbojpeg.h>

using namespace std;

int main()
{
   int frames = 10000;
   int height = 1920;
   int width  = frames *2;

   // Buffer in which to assemble complete output image (RGB), e.g. 20000x1920
   unsigned char *img = new unsigned char [width*height*3];

   // Buffer for one scanline image 1920x2 (RGB)
   unsigned char *scanline = new unsigned char[2*height*3];

   // Output column
   int ocol=0;

   // Read frames from `ffmpeg` fed into us like this:
   // ffmpeg -threads 4 -y -i video.mov -frames 10000 -vf "transpose=1" -f image2pipe -vcodec rawvideo -pix_fmt rgb24 - | ./stitch
   for(int f=0;f<10000;f++){
      // Read one scanline from stdin, i.e. 2x1920 RGB image...
      ssize_t bytesread = read(STDIN_FILENO, scanline, 2*height*3);

      // ... and place into finished frame
      // ip is pointer to input image
      unsigned char *ip = scanline;
      for(int row=0;row<height;row++){
         unsigned char *op = &(img[(row*width*3)+3*ocol]);
         // Copy 2 RGB pixels from scanline to output image
         *op++ = *ip++; // red
         *op++ = *ip++; // green
         *op++ = *ip++; // blue
         *op++ = *ip++; // red
         *op++ = *ip++; // green
         *op++ = *ip++; // blue
      }
      ocol += 2; 
   }

   // Now encode to JPEG with turbo-jpeg
   const int JPEG_QUALITY = 75;
   long unsigned int jpegSize = 0;
   unsigned char* compressedImage = NULL;
   tjhandle _jpegCompressor = tjInitCompress();

   // Compress in memory
   tjCompress2(_jpegCompressor, img, width, 0, height, TJPF_RGB,
          &compressedImage, &jpegSize, TJSAMP_444, JPEG_QUALITY,
          TJFLAG_FASTDCT);

   // Clean up
   tjDestroy(_jpegCompressor);

   // And write to disk
   ofstream f("result.jpg", ios::out | ios::binary);
   f.write (reinterpret_cast<char*>(compressedImage), jpegSize);
}

Заметки:

Примечание 1: Для того, чтобы предварительно распределить выходное изображение, программе необходимо знать, сколько кадров поступает заранее - я не параметризировал это, я просто жестко запрограммировал 10000, но это должно быть достаточно легко изменить.

Один из способов определения количества кадров в видеопоследовательности заключается в следующем:

ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_frames -of default=nokey=1:noprint_wrappers=1 video.mov

Примечание 2: обратите внимание, что я собрал код с парой переключателей для повышения производительности:

g++-8 -O3 -march=native stitch.cpp -o stitch

Примечание 3: если вы работаете в Windows, вам может потребоваться повторно открыть stdin в двоичном режиме перед выполнением:

read(STDIN_FILENO...)

Примечание 4: Если вы не хотите использовать turbo-jpeg, вы можете удалить все после окончания основного цикла и просто отправить NetPBM PPM Изображение в ImageMagick через канал, и пусть он делает запись в формате JPEG. Это будет выглядеть примерно так:

writeToStdout("P6 20000 1920 255\n");
writeToStdout(img, width*height*3);

Тогда вы будете работать с:

ffmpeg ... | ./stitch | magick ppm:-  result.jpg

Я сгенерировал пример видео в 10000 кадров и провел несколько тестов. Очевидно, что моя машина не совпадает с вашей спецификацией, поэтому результаты не сопоставимы напрямую, но я обнаружил, что быстрее ffmpeg перенести видео и передать его в ImageMagick в виде необработанных кадров RGB24.

Я обнаружил, что могу преобразовать 10-секундный фильм в JPEG размером 20 000x1920 пикселей за 10,3 с:

ffmpeg -threads 4 -y -i video.mov -frames 10000 -vf "transpose=1" -f image2pipe -vcodec rawvideo -pix_fmt rgb24 - | convert -depth 8 -size 2x1920 rgb:- +append result.jpg

Полученное изображение выглядит так:


Я создал видео, как это с CImg, По сути, он просто рисует красный / зеленый / синий отросток последовательно по кадру, пока не достигнет правого края, а затем снова начнет с левого края:

#include <iostream>
#include "CImg.h"

using namespace std;
using namespace cimg_library;

int main()
{
   // Frame we will fill
   CImg<unsigned char> frame(1920,2,1,3);

   int width =frame.width();
   int height=frame.height();

   // Item to draw in frame - created with ImageMagick
   // convert xc:red xc:lime xc:blue +append -resize 256x2\! splodge.ppm
   CImg<unsigned char> splodge("splodge.ppm");

   int offs  =0;

   // We are going to output 10000 frames of RGB raw video
   for(int f=0;f<10000;f++){
      // Generate white image
      frame.fill(255);

      // Draw coloured splodge at correct place
      frame.draw_image(offs,0,splodge);
      offs = (offs + 1) % (width - splodge.width());

      // Output to ffmpeg to make video, in planar GBR format
      // i.e. run program like this
      // ./main | ffmpeg -y -f rawvideo -pixel_format gbrp -video_size 1920x2 -i - -c:v h264 -pix_fmt yuv420p video.mov
      char* s=reinterpret_cast<char*>(frame.data()+(width*height));   // Get start of G plane
      std::cout.write(s,width*height);                                // Output it
      s=reinterpret_cast<char*>(frame.data()+2*(width*height));       // Get start of B plane
      std::cout.write(s,width*height);                                // Output it
      s=reinterpret_cast<char*>(frame.data());                        // Get start of R plane
      std::cout.write(s,width*height);                                // Output it
   }
}

Растрескивание составляет 192x2 пикселей и выглядит так:

Другие вопросы по тегам