Долго я обещал связать MJPG-Streamer и OpenCV и вот свершилось.
Код упрощен до безобразия и содержит в себе лишь нужный минимум. Это сделано специально, для того чтобы вы смогли встраивать его в свои приложения не таща за собой куски лишнего кода.
В создании кода мне безгранично помогли:
Andrey Smorodov и его сайт
http://www.compvision.ru/forum/index.phpи
OverQuantum http://overquantum.livejournal.com/.
Возможно, а как я уже сейчас понимаю, это зависит в основном от камеры, вы столкнетесь с ошибкой, которая изображена во вложении. На своей Logitech C270 у меня было также, а вот уже на Logitech C100 ошибок подобного рода не наблюдалось. Ниже решение этой проблемы. Ответ OverQuantum-а до такой степени профессиональный, что я приведу его полностью.
Цитата:
Это не лишние байты, это padding - дополнение битовой строки до полных байт единицами, которое необходимо перед маркером.
Ошибку вызывает padding из 8 бит, который в общем-то, излишен, стандартом не требуется, но не запрещён явно.
Поэтому обычные просмотрщики не ругаются, а вот (древняя) библиотека libjpeg - ругается.
OpenCV использует эту библиотеку.
Кстати, сообщение неправильное, лишний байт всегда 1 (всегда 2, если считать escaping FF->FF00).
ИМХО, есть 4 способа решить проблему:
1) Исправить код, генерирующий такие jpeg-и.
Насколько я понимаю, их даже не mjpg-streamer делает, а сама камера или её драйвер.
2) Исправить код OpenCV, загружающий jpeg-и.
Насколько я вижу в исходниках последней версии под Unix, нужный файл
\OpenCV-2.3.1a.tar.bz2\OpenCV-2.3.1\3rdparty\libjpeg\jdmarker.c
Функция next_marker (j_decompress_ptr cinfo), код:
if (cinfo->marker->discarded_bytes != 0) {
WARNMS2(cinfo, JWRN_EXTRANEOUS_DATA, cinfo->marker->discarded_bytes, c);
cinfo->marker->discarded_bytes = 0;
}
Я бы закоментил строку WARNMS2(..)
3) Исправлять jpeg-и на лету путём выбрасывания лишнего padding-а
Насколько я вижу, padding ставится всегда, и вот когда он не нужен (битовая строка и так закончилась на границе байт) - ставится полный байт.
Соотв. нужно заменить все последовательности FF 00 FF Di на FF Di (i = 0 ..7)
Однако это может повредить изображение - в случае если битовая строка, кодирующая изображения заканчивалась на несколько бит равных 1 и padding тоже из бит 1, в этом случае мы получим такую же пару FF 00 перед маркером, но это уже будет полностью корректный padding, который нельзя отрезать.
Возникнуть это может довольно редко - в MCU блоке должен быть не нулевой последний 63-й коэффициент, его значение должно заканчиваться на несколько бит 1, они должны залезть через границу байт и попасть аккурат перед reset-маркером.
Тем не менее, исключить это нельзя и нужно потестировать на большом объёме кадров, возникает ли повреждение.
4) Исправлять jpeg-и на лету путём полного перекодирования.
Например тем же libjpeg можно игнорировать ошибку, распаковать изображение из файла в DCT коэффициенты и запаковать обратно в jpeg-файл.
Я проверил второй и третий способы. Как бы не был красив третий, он не даёт, к сожалению, стабильного результата по описанным выше причинам.
Посему просто комментируете WARNMS2(cinfo, JWRN_EXTRANEOUS_DATA, cinfo->marker->discarded_bytes, c);
и пересобираете весь OpenCV. Сделать это не сложно. Хорошо описано здесь
http://robocraft.ru/blog/computervision/700.htmlПару слов про сам код. К сожалению, он не выдает стабильное FPS. С чем это связано не знаю.
Было бы здорово, если кто-то переписал его под DirectShow для приема реального потока, а не так как реализовал это я.
Если у вас возникнут вопросы, смело обращайтесь. Я всегда к вашим услугам.
Код:
#pragma comment(lib,"Wininet.lib")
#include "stdafx.h"
#include <cv.h>
#include <highgui.h>
#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include "WinInet.h"
#include <string.h>
#include "cxcore.h"
#define strncasecmp _strnicmp
struct tmjpeg
{
unsigned char *pbuff;
DWORD ibuffsize;
unsigned char *pmess;
DWORD imesssize;
IplImage* image;
int width;
int height;
HINTERNET hSession;
HINTERNET hConnect;
HINTERNET hRequest;
char szUrl[MAX_PATH];
};
tmjpeg mjpeg;
static int PeekData(void)
{
int ilen;
// узнаём размер следующего пакета
InternetQueryDataAvailable(mjpeg.hRequest,&mjpeg.imesssize,0,0);
// выходим если размер нулевой
if(mjpeg.imesssize<=0)
return -1;
// зарезервируем массив байт для очередной порции
mjpeg.pmess=new unsigned char[mjpeg.imesssize];
if(mjpeg.pmess==NULL)
return -1;
// эта переменка будет получать число считанных байт
DWORD nbr=0;
// Если считывание удается
if (!InternetReadFile(mjpeg.hRequest,mjpeg.pmess,mjpeg.imesssize,&nbr))
return -1;
//Формируем временный буфер размером старый буфер+ новый пакет
ilen=mjpeg.ibuffsize+mjpeg.imesssize;
unsigned char *ps= new unsigned char[ilen];
if(ps==NULL)
return -1;
//Если это не первичный запуск
if(mjpeg.ibuffsize>0)
{
// Копируем старый буфер
memcpy(ps,mjpeg.pbuff,mjpeg.ibuffsize);
// Освобождаем память старого буфера
free(mjpeg.pbuff);
}
// Копируем новый пакет
memcpy(ps+mjpeg.ibuffsize,mjpeg.pmess,mjpeg.imesssize);
// Освобождаем память пакета
free(mjpeg.pmess);
//Переназначаем буфер
mjpeg.pbuff=ps;
// Устанавливаем новый размер буфера
mjpeg.ibuffsize=ilen;
return mjpeg.imesssize;
}
bool CreateMJPEGCameraCapture(char *szUrl, int width, int heigth)
{
mjpeg.hSession = InternetOpen(TEXT("MJPG listener"),INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
mjpeg.width=width;
mjpeg.height=heigth;
strcpy(mjpeg.szUrl,szUrl);
if(mjpeg.hSession)
return true;
else
return false;
}
IplImage* QueryMJPEGFrame(void)
{
mjpeg.ibuffsize=0;
mjpeg.imesssize=0;
mjpeg.pmess=NULL;
mjpeg.hRequest = InternetOpenUrlA(mjpeg.hSession, (LPCSTR)mjpeg.szUrl, NULL, 0, 0, 0);
if(mjpeg.hRequest)
{
DWORD Size=0;
// Выясняем по сколько портал нам будет высылать байт в очередной порции
InternetQueryDataAvailable(mjpeg.hRequest,&Size,0,0);
while(PeekData()>0);
//printf("Bufflen=%d\n\n",mjpeg.ibuffsize);
CvMat* mat = cvCreateMatHeader(1,mjpeg.ibuffsize, CV_8UC1 );
cvCreateData( mat );
cvSetData(mat,mjpeg.pbuff,mjpeg.ibuffsize);
// декодируем массив в картинку
if(mjpeg.image!=NULL)
cvReleaseImage(& mjpeg.image);
mjpeg.image = cvDecodeImage(mat);//,CV_LOAD_IMAGE_COLOR);
cvReleaseData(mat);
if(mjpeg.pbuff)
free(mjpeg.pbuff);
mjpeg.pbuff=NULL;
InternetCloseHandle(mjpeg.hRequest);
} else
return NULL;
return mjpeg.image;
}
bool ReleaseMJPEGCapture(void)
{
if(mjpeg.hSession)
{
InternetCloseHandle(mjpeg.hSession);
}
if(mjpeg.image!=NULL)
cvReleaseImage(&mjpeg.image);
if(mjpeg.pbuff!=NULL)
free(mjpeg.pbuff);
return true;
}
int _tmain(int argc, _TCHAR* argv[])
{
IplImage* frame;
if(!CreateMJPEGCameraCapture("http://192.168.1.120:8080/?action=snapshot",640,480))
{
return 0;
}
cvNamedWindow("original",CV_WINDOW_AUTOSIZE);
while(true)
{
// получаем кадр
frame=QueryMJPEGFrame();
if(frame==NULL)
{
printf("Error: No frame\n");
break;
}
// показываем
cvShowImage("original",frame);
char c = cvWaitKey(33);
if (c == 27)
{ // нажата ESC
break;
}
}
// удаляем окно
cvDestroyWindow("original");
// освобождаем ресурсы
ReleaseMJPEGCapture();
return 0;
}