Первоначально опубликовано на 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.