Первоначально опубликовано на blog.lemberg.co.uk.
Когда вы выполняете хороший «карточный фокус», все детали и сложность должны быть невидимы для наблюдателя. Магия должна казаться гладкой и естественной! Сегодня мы заглянем за кулисы. Войдите в прямую трансляцию.
Большинство социальных сетей поддерживают прямую трансляцию. Такие, как Youtube, Facebook, Snapchat, Instagram… Режим LIVE в наши дни стал обычным явлением. Если вашего продукта еще нет - добавьте его в список Todo!
Live Stream (Broadcast) - это не одноранговая модель потока данных. Это более сложное решение. Вот о чем мы и поговорим. За кулисами. Пройдемся по классифицированному «LIVE».
Как транслировать в прямом эфире?
Есть 3 этапа, на которых создается поток: захват, кодирование и запуск. Теперь подробнее о каждом из них.
- Захват - на этом этапе мы захватываем входной поток как необработанные данные. Это может быть файл или другой поток, используемый в качестве источника.
- Encode - подготовка и форматирование входного потока. Кодируйте в формат пикселей RBG / HSV, чтобы иметь возможность анализировать и редактировать каждый кадр. Сжатие вывода с помощью кодека для повышения производительности и уменьшения задержки.
- Go Live - создайте конечную точку общего потока с поддержкой нескольких подключений.
Захватывать
FFMPEG / OPENCV ЗАХВАТ
Так что готовить? Как создать успешную точку и обработать первый кадр потока. Основной инструмент для этого - FFmpeg lib. FFmpeg - это проект бесплатного программного обеспечения, который производит библиотеки и программы для работы с мультимедийными данными. Независимо от того, какой источник вы собираетесь использовать с FFmpeg (экран, камера, файл) - вы даже можете настроить его с помощью командной строки:
Mac OS. Список медиаустройств AVFoundation
ffmpeg -f avfoundation -list_devices true -i ""
… Устройство захвата экрана.
Mac OS. Устройство экрана AVFoundation
ffmpeg -f avfoundation -i "1" -pix_fmt yuv420p -r 25 -t 5 /Users/UserName/Downloads/out.mov
Основанная на ffmpeg, openCV lib использует те же принципы для обработки источника потока:
const std::string url = “http://192.168.3.25:1935/live/myStream/ playlist.m3u8"; cv::VideoCapture capture(url); if (!capture->isOpened()) { //Error } cv::namedWindow("Stream", CV_WINDOW_AUTOSIZE); cv::Mat frame; while(stream_enable) { if (!capture->read(frame)) { //Error } cv::imshow("Stream", frame); cv::waitKey(30); }
В результате мы видим окно с текущим захватом потока.
cv :: Mat frame - объект текущего фрейма. объект cv :: Mat - представляет двухмерную пиксельную матрицу с форматом пикселей HSV или BGR. Строки и столбец представляют матрицу пикселей, которая является промежуточным форматом в процессе потоковой передачи.
Видеоанализ
OpenCV - Первоначально разработанный исследовательским центром Intel, на мой взгляд, это величайший скачок в области компьютерного зрения и анализа медиа-данных. Главное, что нужно отметить в OpenCV, - это высокопроизводительный анализ с использованием 2-мерной пиксельной матрицы. Свыше 30 кадров в секунду с высоким качеством составляет около 30 миллионов пикселей в секунду. Вы, должно быть, думаете про себя: Это большая нагрузка, не так ли? Это означает, что анализ должен быть очень быстрым, чтобы ваш процессор работал. Использование многоядерного процесса и оптимизации на низком уровне уступает место сверхбыстрому анализу библиотеки. Определенно это набор инструментов с множеством алгоритмов. OpenCV - это главный инструмент искусственного интеллекта, когда мы говорим о медиа-контенте. Что мы можем анализировать? - Ну в принципе вообще все, что может быть 2д матрицей. Давайте посмотрим на простой код для обнаружения карты в кадре.
Отслеживание объекта
Порог
Пример игральной карты при уменьшении до максимального контраста. Одним словом - ПОРОГ. Этот метод предоставляет черно-белую зону, описывающую область объекта.
Метод порога принимает в качестве параметров входные и выходные кадры, значение порога и стратегию порога.
int threshold_value = 160; int max_BINARY_value = 255; threshold(inFrame, outFrame, threshold_value, max_BINARY_value, THRESH_BINARY);
Порог обеспечивает наиболее удовлетворительный результат в случае высокой контрастности изображения. Также мы могли бы попытаться обнаружить края, чтобы описать объект в кадре. Canny - Edge Detector. Обнаруживает края между большинством цветов, которые различаются значениями и / или контрастностью.
/// Detect edges using canny Canny( inFrame, cannyOut, threshold_value, max_BINARY_value, THRESH_TOZERO );
Воспользовавшись парой методов, мы можем обнаружить контур.
vector > contours; vector hierarchy; findContours(cannyOut, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
Контуры - массив контуров, обнаруженных в кадре. Сравнение, измерение и мы приближаемся к нашей цели.
int threshholdValue = 200; int main() { const std::string url = "/Users/maxvitruk/Documents/Press/video/trailer_1.mp4"; VideoCapture cap(url); if (!cap.isOpened()) { cout << "Failed to open camera" << endl; return 1; } double width = cap.get(CV_CAP_PROP_FRAME_WIDTH); double height = cap.get(CV_CAP_PROP_FRAME_HEIGHT); namedWindow("Original", CV_WINDOW_AUTOSIZE); namedWindow("Thresh", CV_WINDOW_AUTOSIZE); VideoWriter video("/Users/maxvitruk/Downloads/out.avi",CV_FOURCC('M','J','P','G'),10, Size(width,height),true); bool readCamera = false; while (!readCamera) { Mat frame; bool success = cap.read(frame); if (!success) { cout << "Failed to read frame" << endl; break; } Mat detectedFrame = frame.clone(); Mat thresh = frame.clone(); cvtColor(thresh, thresh, COLOR_BGR2GRAY); GaussianBlur(thresh, thresh, Size(1, 1), 100); threshold(thresh, thresh, threshholdValue, 255, THRESH_BINARY); vector > contours; vector hierarchy; findContours(thresh, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); vector > approx(contours.size()); vector boundRects(contours.size()); for (size_t cIndex = 0; cIndex < contours.size(); ++cIndex) { vector contour = contours[cIndex]; double perimeter = arcLength(contour, true); if (perimeter > 500.0 && hierarchy[cIndex][3] == -1){ approxPolyDP(Mat(contour), approx[cIndex], 0.001 * perimeter, true); boundRects[cIndex] = boundingRect(Mat(approx[cIndex])); rectangle(detectedFrame, boundRects[cIndex].tl(), boundRects[cIndex].br(), rectColor, 4, 8, 0); } } imshow("Original", detectedFrame); imshow("Thresh", thresh); video.write(detectedFrame); int key = waitKey(30); switch (key){ case 27: readCamera = true; break; } } destroyWindow("Test"); return 0; }
Обнаружение карты по контуру
Шаг 1. Прочтите видеофайл
VideoCapture cap(url);
Шаг 2.
- Уменьшите цвет до черно-белого
- Размытие рамки до приближенных краев
- Уменьшить контраст
cvtColor(thresh, thresh, COLOR_BGR2GRAY); GaussianBlur(thresh, thresh, Size(1, 1), 100); threshold(thresh, thresh, threshholdValue, 255, THRESH_BINARY);
Шаг 3. Найдите контуры
findContours(thresh, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
Шаг 4. Измерение периметра
double perimeter = arcLength(contour, true);
Распознавание лиц
КАСКАДЕКЛАССИФИКАТОР
WIKI: Каскадирование - это частный случай« ансамблевого обучения , основанного на объединении нескольких классификаторов , с использованием всей информации, собранной из выходных данных данного классификатора, в качестве дополнительной информации для следующего классификатора в каскаде. В отличие от ансамблей для голосования или суммирования, которые представляют собой многоступенчатые экспертные системы, каскадирование является многоступенчатым ».
Название технологии дает нам четкое представление о том, как она работает. Каскадная классификация похожа на рекурсивный поиск требуемой функции в одном кадре. Пример: человеческое лицо состоит из элементарных геометрических форм, которые можно описать с помощью файла XML. Каждая графика имеет абстрактную геометрическую иерархию. Художники используют эту технику для тренировки своих портретных навыков. По сути, мы определяем центр объекта и рисуем ограничивающий овал. Положение глаз, нос ... губы и уши ... каждый следующий узел размещен на основе предыдущего. Переходите от общего к уточнению, чтобы найти лицо на одном кадре.
CASCADECLASSIFIER - результат исследований искусственного интеллекта. Анализируя набор данных и сравнивая, получаем объект классификации.
Обнаружение лиц в OpenCV
string face_cascade_name = "/path/haarcascade_frontalface_alt.xml”; CascadeClassifier face_cascade; void detectAndDrawDetectedFace(Mat* frame) { std::vector faces; Mat frame_gray; //Convert to gray cv::cvtColor(image, frame_gray, COLOR_BGR2GRAY); equalizeHist(frame_gray, frame_gray); //Scale to improve performance resize(frame_gray, frame_gray, cv::Size(), scale, scale); // Detect faces face_cascade.detectMultiScale(frame_gray, faces, 1.1, 2, 0 | CASCADE_SCALE_IMAGE, Size(30, 30)); //Scale face params to original image size float bSale = 1 / scale; for (auto const& face : faces){ Point pt1(face.x * bSale, face.y * bSale); // Display detected faces on main window - live stream from camera Point pt2((face.x * bSale + face.height * bSale), (face.y * bSale + face.width * bSale)); //Draw rectangle(image, pt1, pt2, Scalar(0, 255, 0), 2, 8, 0); } frame_gray.release(); }
Haarcascade_frontalface_alt.xml - объединить функцию модели данных человеческого лица.
Через сеть
Процесс получения и обработки завершен. Пришло время передать это FFMPEG. Давайте создадим выходной поток.
Нам знаком фрейм cv :: Mat. Он отлично подходит для анализа, но не для передачи данных. Пора использовать кодек - сжатие. Во-первых, вы можете определить разницу между кадром cv :: mat и форматом пикселей AVPicture. Yuv420p для AVPicture и BGR для cv :: Mat. Чтобы добиться быстрого вывода, мы упаковываем поток через кодек H.264 или MPEG-4.
INIT Stream
WIKI: протокол потоковой передачи в реальном времени (RTSP) - это протокол управления сетью, предназначенный для использования в развлекательных и коммуникационных системах для управления серверами потокового мультимедиа. Протокол используется для установления и управления медиа-сеансами между конечными точками. Клиенты медиа-серверов выдают команды в стиле видеомагнитофона, такие как воспроизведение, запись и пауза, для облегчения управления в реальном времени потоковой передачей мультимедиа от сервера к клиенту (видео по запросу) или от клиента к серверу (запись голоса) .
Инициализировать выходной поток. Определите кодек. Настройте буферы. Настройте заголовки.
/***************** Init Stream *****************/ int init_stream() { int ret; /* Initialize libavcodec, and register all codecs and formats. */ av_register_all(); avformat_network_init(); av_log_set_level(AV_LOG_DEBUG); /* allocate the output media context */ avformat_alloc_output_context2(&oc, NULL, "rtsp", filename); if (!oc) { std::cout<<"Could not read output from file extension: using MPEG."<< std::endl; avformat_alloc_output_context2(&oc, NULL, "mpeg", filename); } if (!oc){ std::cout<<"Failed to ini output context"<< std::endl; return FAILED_OUTPUT_INIT; } fmt = oc->oformat; if(!fmt) { std::cout<<"Error creating outformat\n"<< std::endl; } /* Add the audio and video streams using the default format codecs * and initialize the codecs. */ video_st = NULL; fmt->video_codec = CODEC_ID; std::cout<< "Codec = " << avcodec_get_name(fmt->video_codec) <video_codec != AV_CODEC_ID_NONE) { video_st = add_stream(oc, &video_codec, fmt->video_codec); } /* Now that all the parameters are set, we can open the audio and * video codecs and allocate the necessary encode buffers. */ if (video_st) { open_video(oc, video_codec, video_st); } av_dump_format(oc, 0, filename, 1); char errorBuff[80]; if (!(fmt->flags & AVFMT_NOFILE)) { ret = avio_open(&oc->pb, filename, AVIO_FLAG_WRITE); if (ret < 0) { std::cout << "Could not open outfile: " << filename << "\n" << "Error: " << av_make_error_string(errorBuff,80,ret) << "\n" << endl; return FAILED_OUTPUT; } } std::cout << "Stream: " << filename << "\n" << "format: " << oc->oformat->name << "\n" << "vcodec: " << video_codec->name << "\n" << "size: " << dst_width << 'x' << dst_height << "\n" << "fps: " << av_q2d(dst_fps) << "\n" << "pixfmt: " << av_get_pix_fmt_name(video_st->codec->pix_fmt) << "\n" << endl; ret = avformat_write_header(oc, NULL); if (ret < 0) { std::cout << "Error occurred when writing header: " << av_make_error_string(errorBuff,80,ret) << "\n" << endl; return FAILED_OUTPUT_HEADER; } return 0; }
Настроить кодек
/*********************** CONFIGURE OUTPUT STREAM ************************/ AVStream *add_stream(AVFormatContext *oc, AVCodec **codec, enum AVCodecID codec_id) { AVCodecContext *c; AVStream *st; /* find the encoder */ *codec = avcodec_find_encoder(codec_id); if (!(*codec)) { std::cout << "Could not find encoder for" << avcodec_get_name(codec_id) << std::endl; exit(1); } st = avformat_new_stream(oc, *codec); if (!st) { std::cout << "Could not allocate stream" << std::endl; exit(1); } st->id = oc->nb_streams-1; c = st->codec; c->codec_id = codec_id; c->bit_rate = 800000; /* Resolution must be a multiple of two. */ c->width = dst_width; c->height = dst_height; /* timebase: This is the fundamental unit of time (in seconds) in terms * of which frame timestamps are represented. For fixed-fps content, * timebase should be 1/framerate and timestamp increments should be * identical to 1. */ c->time_base.den = STREAM_FRAME_RATE; c->time_base.num = 1; c->gop_size = 12; /* emit one intra frame every twelve frames at most */ c->pix_fmt = STREAM_PIX_FMT; if (c->codec_id == AV_CODEC_ID_MPEG2VIDEO) { /* just for testing, we also add B frames */ c->max_b_frames = 2; } if (c->codec_id == AV_CODEC_ID_MPEG1VIDEO) { /* Needed to avoid using macroblocks in which some coeffs overflow. * This does not happen with normal video, it just happens here as * the motion of the chroma plane does not match the luma plane. */ c->mb_decision = 2; } /* Some formats want stream headers to be separate. */ if (oc->oformat->flags & AVFMT_GLOBALHEADER) { c->flags |= CODEC_FLAG_GLOBAL_HEADER; } return st; }
Конфигурация подключения. Уровень сжатия, качество потока
/*********************** OPEN OUTPUT CONNECTION ************************/ void open_video(AVFormatContext *oc, AVCodec *codec, AVStream *st) { int ret; AVCodecContext *c = st->codec; /* open the codec */ AVDictionary *opts = NULL; /* Change options to trade off compression efficiency against encoding speed. If you specify a preset, the changes it makes will be applied before all other parameters are applied. You should generally set this option to the slowest you can bear. Values available: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo. */ av_dict_set(&opts, "preset", "superfast", 0); /* Tune options to further optimize them for your input content. If you specify a tuning, the changes will be applied after --preset but before all other parameters. If your source content matches one of the available tunings you can use this, otherwise leave unset. Values available: film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency. */ av_dict_set(&opts, "tune", "zerolatency", 0); /* open the codec */ ret = avcodec_open2(c, codec, &opts); if (ret < 0) { std::cout << "Could not open video codec" << std::endl; exit(1); } /* allocate and init a re-usable frame */ frame = av_frame_alloc(); pFrameBGR =av_frame_alloc(); if (!frame) { std::cout << "Could not allocate video frame" << std::endl; exit(1); } frame->format = c->pix_fmt; frame->width = c->width; frame->height = c->height; }
Подготовьте фрейм cv :: Mat для записи. Используйте контекст масштабирования программного обеспечения (SwsContext) для создания AVPicture из cv :: Mat.
/***************** WRITE VIDEO FRAMES *****************/ void write_video_frame(AVFormatContext *oc, AVStream *st, int play) { int ret; AVCodecContext *c = st->codec; int numBytesYUV = av_image_get_buffer_size(STREAM_PIX_FMT, dst_width,dst_height,1); if(!bufferYUV) { bufferYUV = (uint8_t *)av_malloc(numBytesYUV*sizeof(uint8_t)); } /* Assign image buffers */ avpicture_fill((AVPicture *)pFrameBGR, image.data, AV_PIX_FMT_BGR24, dst_width, dst_height); avpicture_fill((AVPicture *)frame, bufferYUV, STREAM_PIX_FMT, dst_width, dst_height); if (!sws_ctx) { /* Initialise Software scaling context */ sws_ctx = sws_getContext(dst_width, dst_height, AV_PIX_FMT_BGR24, dst_width, dst_height, STREAM_PIX_FMT, SWS_BILINEAR, NULL, NULL, NULL ); } /* Convert the image from its BGR to YUV */ sws_scale(sws_ctx, (uint8_t const * const *)pFrameBGR->data, pFrameBGR->linesize, 0, dst_height, frame->data, frame->linesize); AVPacket pkt = { 0 }; int got_packet; av_init_packet(&pkt); /* encode the image */ frame->pts = frame_count; ret = avcodec_encode_video2(c, &pkt, play ? NULL : frame, &got_packet); if (ret < 0) { std::cout << "Error while encoding video frame" << std::endl; exit(1); } /* If size is zero, it means the image was buffered. */ if (got_packet) { ret = write_frame(oc, &c->time_base, st, &pkt); } else { if (play) video_is_eof = 1; ret = 0; } if (ret < 0) { std::cout << "Error while writing video frame" << std::endl; exit(1); } frame_count++; }
Напишите каждый кадр.
int write_frame(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt) { /* rescale output packet timestamp values from codec to stream timebase */ pkt->pts = av_rescale_q_rnd(pkt->pts, *time_base, st->time_base, AVRounding(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX)); pkt->dts = av_rescale_q_rnd(pkt->dts, *time_base, st->time_base, AVRounding(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX)); pkt->duration = av_rescale_q(pkt->duration, *time_base, st->time_base); pkt->stream_index = st->index; /* Write the compressed frame to the media file. */ return av_interleaved_write_frame(fmt_ctx, pkt); }
Заключение
FFMpeg / OpenCV - это сила в манипулировании медиа-контентом. Низкий уровень инструментов (C / C ++) позволяет анализировать видео в реальном времени. Это действительно похоже на волшебство - все детали внутри.
Надеюсь, вы нашли эту статью полезной, не забудьте оставить свои комментарии под статьей. Если у вас есть идея проекта, но вы не знаете, с чего начать, мы всегда готовы вам помочь.
Изначально опубликовано на blog.lemberg.co.uk.