데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      FFmpeg Custom Video Filter 만들기 #2

      fromDK 23.05.16
      2,250 9 0

      이전 글에서는 Chat GPT에 FFmpeg Custom Video Filter 만드는 방법을 물어 봤습니다.

      결과물은 그럴싸하게 나왔으나, 정상적으로 동작하지는 않았어요. 역시나, 아직은 개발자가 필요하네요 ^^


      그럼 이번엔 구글에 검색해서 FFmpeg Custom Video Filter 만드는 법을 찾아봤습니다.


      FFmpeg Docs - Writing Filters

      위의 FFmpeg Document를 보니 Custom Video Filter 만드는 법이 잘 설명이 되어 있습니다.


      되도록 위의 문서에 없는 내용을 설명해보도록 하겠습니다.


      제가 요청 받은 FFmpeg의 Custom Video Filter 요구사항은 아래와 같습니다.

      • Decoding 된 영상을 ML기반 Python 엔진에서 필터링 후 다시 FFmpeg에 전달 할 수 있을 것

      • IPC로 프로세스 분리하여 동작 할 것

      아래 그림과 같이 Custom Video Filter를 FFmpeg에 추가하고, IPC를 통해 외부 Python 엔진과 데이터를 주고 받을 수 있는 구조로 설계하였습니다.

      이렇게 함으로써, FFmpeg의 모드 설비를 이용할 수 있고, ML기반의 Python엔진들을 간단하게 붙여서 동작 시킬 수 있으며, GPL 라이선스에 따른 소스 공개의무도 어느정도 우회 할 수 있었습니다 ^^;;.

      ffmepg.png

      IPC 설비로는 named pipe(FIFO)를 사용하였습니다. Windows 나 linux에서 모두 지원하고, 간단하게 구현가능한 장점이 있습니다.


      Video Filter 기본 구조

      우선 FFmpeg의 Filter구조를 보면 아래 5개의 함수만 구현 하면 됩니다.



      init()

      초기화에 사용됨. 필요한 메모리 및 리소스 할당

      uninit()

      init()에서 할당된 리소스 해제

      query_formats()

      지원하는 비디오 Pixel 포멧 정보 전달. Video Filter간 Pixel Format Negotiation에 사용됨.

      config_props()

      config properties. 비디오 필터간 연결시 pad 로 연결이 되는데 이 때 호출됨. 입력 pad에서 Pixel Format Negotiation 된 포멧을 확인하거나, 출력 pad 의 영상 크기 등을 지정 할 수 있음.

      filter_frame()

      실제 필터링이 이루어지는 함수. 이전 필터로부터 데이터가 전달 될때 마다 호출된다.

      image.png

      위의 그림과 같이 Filter는 입력 Pad와 출력 Pad를 통해 데이터를 받아 처리 후 다음 필터로 전달하게 되어 있다.

      만약 Filter 1의 Output Pad의 Pixel 포멧이 AV_PIX_FMT_RGB32 와 같이 RGB로만 출력하는 경우,

      Filter 2의 query_format()을 통한 지원하는 Pixel포멧을 확인해보니 AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P와 같이

      지원하는 포멧이 다른 경우 해당 필터는 연결 할 수 없습니다.


      또한, 지원하는 포멧이 여러개 인경우 Negotiation을 통해서 Pixel Format이 결정되게 됩니다.


      Filter Options

      Filter에 옵션을 전달 할 수 있도록 아래와 같이 추가 합니다.

      간단히 설명하면 Bypass 모드나 IPC로 Python 엔진을 통해 필터링 할 것 인지 설정 할 수 있는 옵션과, 입출력 Named pipe의 경로를 지정하는 옵션을 추가 하였습니다.

      #define OFFSET(x) offsetof(SupernovaContext, x)
      #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM
      static const AVOption supernova_options[] = {
          { "mode", "set mode", OFFSET(mode), AV_OPT_TYPE_INT, {.i64=MODE_BYPASS}, 0, NB_MODES-1, FLAGS, "mode" },
              { "bypass", "bypass mode",  0, AV_OPT_TYPE_CONST, {.i64=MODE_BYPASS},    INT_MIN, INT_MAX, FLAGS, "mode" },
              { "ipc", "ipc mode",        0, AV_OPT_TYPE_CONST, {.i64=MODE_IPC}, INT_MIN, INT_MAX, FLAGS, "mode" },
          {"in_pipe",   "set supernova input pipe", OFFSET(in_pipe_path), AV_OPT_TYPE_STRING, {.str="supernova_in"},  0, 0, FLAGS },
          {"out_pipe",   "set supernova output pipe", OFFSET(out_pipe_path), AV_OPT_TYPE_STRING, {.str="supernova_out"},  0, 0, FLAGS },
          { NULL }
      };

      위의 옵션들은 아래와 같이 FFmpeg CLI로 사용할 수 있습니다.

      $ ffmpeg -i test.mp4 -vf supernova=mode=ipc:in_pipe=supernova_in:out_pipe=supernova_out output.mp4


      init / uninit

      init 함수에서 ipc mode인 경우 named pipe를 생성하고, fork 후 python 엔진을 실행 합니다.

      종료 시 signal 전달을 위해 parent와 child의 pid를 각각 저장 후, 종료시 시그널로 알림을 줄 수 있도록 하였습니다.

      실제로는 성능 향상을 위해 Named pipe오픈 후, pipe의 버퍼 크기를 조정하여 영상과 같은 큰 사이즈의 버퍼를 전달할때 성능 향상이 될 수 있도록 추가 하였습니다.

      static int init_pipe(AVFilterContext *ctx, SupernovaContext *supernova)
      {
          int ret;
          ret = mkfifo(supernova->in_pipe_path, 0666);
          if (ret < 0) {
              av_log(ctx, AV_LOG_WARNING, "failed to make %s pipe %d. it would be already created.", supernova->in_pipe_path, ret);
          }
      
          ret = mkfifo(supernova->out_pipe_path, 0666);
          if (ret < 0) {
              av_log(ctx, AV_LOG_WARNING, "failed to make %s pipe %d. it would be already created.", supernova->out_pipe_path, ret);
          }
      
          pid_t ffmpeg_pid = getpid();
          pid_t pid = fork();
          if (pid == 0) {
              av_log(ctx, AV_LOG_DEBUG, "start supernova engine");
      
              char pid_str[32];
              sprintf(pid_str, "%d", ffmpeg_pid);
      
              ret = execl("/usr/bin/python", "python", supernova->supernova_engine,
                          "--in_fifo", supernova->in_pipe_path,
                          "--out_fifo", supernova->out_pipe_path,
                          "--parent_pid", pid_str,
                          NULL);
      
              if (ret != 0) {
                  av_log(ctx, AV_LOG_ERROR, "failed to start supernova engine(%s) : %d\n",
                      supernova->supernova_engine, ret);
                  return ret;
              }
          } else {
              supernova->engine_pid = pid;
      
              av_log(ctx, AV_LOG_DEBUG, "waiting for supernova engine");
              if ((supernova->in_pipe = open(supernova->in_pipe_path, O_WRONLY | O_NOCTTY)) < 0) {
                  av_log(ctx, AV_LOG_ERROR, "failed to open supernova input pipe : %s", supernova->in_pipe_path);
              }
              if ((supernova->out_pipe = open(supernova->out_pipe_path, O_RDONLY | O_NOCTTY)) < 0) {
                  av_log(ctx, AV_LOG_ERROR, "failed to open supernova output pipe : %s", supernova->out_pipe_path);
              }
          }
          return 0;
      }
      
      
      static av_cold int init(AVFilterContext *ctx)
      {
          SupernovaContext *supernova = ctx->priv;
          int ret = 0;
          if (supernova->mode == MODE_IPC) {
              ret = init_pipe(ctx, supernova);
          }
          return ret;
      }


      uninit의 경우 pipe를 close하고 엔진에 SIGQUIT시그널을 전달하여 정상 종료 되도록 신호를 보내고, 종료 될때 까지 대기 합니다. 또한 할당된 리소스들도 해제 합니다.

      static void uninit_pipe(AVFilterContext *ctx, SupernovaContext *supernova)
      {
          close(supernova->in_pipe);
          close(supernova->out_pipe);
      
          int ret = 0;
          ret = kill(supernova->engine_pid, 3); // 3 : SIGQUIT
          if (ret ) {
              kill(supernova->engine_pid, 9);
          }
      
          int status;
          while (waitpid(supernova->engine_pid, &status, WNOHANG) > 0)
              usleep(10*1000);
      
          unlink(supernova->in_pipe_path);
          unlink(supernova->out_pipe_path);
      }
      
      static av_cold void uninit(AVFilterContext *ctx)
      {
          SupernovaContext *supernova = ctx->priv;
      
          if (supernova->mode == MODE_IPC) {
              uninit_pipe(ctx, supernova);
          }
      
          if (supernova->temp_data) {
              free(supernova->temp_data);
              supernova->temp_data = NULL;
          }
      }


      query_formats

      본 필터가 지원하는 포멧을 물어볼때 아래와 같이 AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P 포멧을 지원한다고 알려줍니다.

      static int query_formats(AVFilterContext *ctx)
      {
          const SupernovaContext *supernova = ctx->priv;
          static const enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P, AV_PIX_FMT_NONE};
          return ff_set_common_formats_from_list(ctx, pix_fmts);
      }


      config_props

      해당 필터가 영상의 해상도를 2배 Scaling 하는 모듈인 경우, 아래와 같이 출력 링크의 해상도를 입력의 2배로 설정합니다.

      
      static int config_props(AVFilterLink *outlink)
      {
          AVFilterLink *inlink  = outlink->src->inputs[0];
          enum AVPixelFormat outfmt = outlink->format;
      
          outlink->w = inlink->w * 2;
          outlink->h = inlink->h * 2;
          outlink->sample_aspect_ratio = inlink->sample_aspect_ratio;
          outlink->time_base = inlink->time_base;
      
          return 0;
      }


      filter_frame

      실제 필터링이 이루어지는 함수입니다. bypass모드인 경우 입력 영상을 바로 출력 버퍼에 복사하여 출력하고,

      IPC모드 인경우 Pipe에 write() 후 python엔진에서 필터링을 수행하고 다시 read()로 영상을 수신하여 다시 출력 버퍼에 복사하는 방법을 이용하였습니다.

      우선 테스트로 temp_data에 메모리를 할당하고 이 메모리를 python엔진에 전달 및 수신하는 방법을 이용하였습니다.

      read(), write()함수는 기본적으로는 block모드로 동작하므로, 종료시 block모드에서 나올 수 가 없어서 강제종료해야하는 경우가 있습니다.

      이를 방지하게 위해 실제 구현에서는 select(), epoll()등을 이용한 multiplex IO나 non-block 모드로 read()/write()함수를 동작시켜야 합니다.

      필터링이 끝난 버퍼를 다시 출력 버퍼에 복사 시에는 av_image_copy_plane()을 이용하였습니다.

      영상의 복사 시에는 av_image_copy(), av_image_copy_to_buffer() 등 다양한 함수들이 있으므로 적절한 함수를 이용하면 됩니다.

      필터링이 끝난 영상은 ff_filter_frame()함수로 다음 필터로 영상을 전달 합니다.

      static int filter_frame(AVFilterLink *inlink, AVFrame *in)
      {
          AVFilterContext *ctx = inlink->dst;
          SupernovaContext *supernova = ctx->priv;
          AVFilterLink *outlink = ctx->outputs[0];
          AVFrame *out;
          int nb_planes = av_pix_fmt_count_planes(inlink->format);
          int ret;
          int chroma_x_shift, chroma_y_shift;
          ret = av_pix_fmt_get_chroma_sub_sample(inlink->format, &chroma_x_shift, &chroma_y_shift);
          if (ret)
              return ret;
      
          out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
          if (!out) {
              av_frame_free(&in);
              return AVERROR(ENOMEM);
          }
          av_frame_copy_props(out, in);
      
          // filtering
          if (supernova->mode == MODE_BYPASS) {
              for (int p = 0; p < nb_planes; p++) {
      
                  int x_shift = (p == 0) ? 0 : chroma_x_shift;
                  int y_shift = (p == 0) ? 0 : chroma_y_shift;
      
                  av_image_copy_plane(
                      out->data[p],
                      out->linesize[p],
                      in->data[p],
                      in->linesize[p],
                      in->width >> x_shift,
                      in->height >> y_shift
                  );
              }
          } else if (supernova->mode == MODE_IPC) {
              int offset = 0;
              int length = 0;
              int size = (inlink->w * inlink->h) + (inlink->w >> chroma_x_shift) * (inlink->h >> chroma_y_shift) * (nb_planes - 1);
      
              if (supernova->temp_data == NULL) {
                  supernova->temp_data = (uint8_t*)malloc(size);
              }
      
              offset = 0;
              for (int p = 0; p < nb_planes; p++) {
                  int x_shift = (p == 0) ? 0 : chroma_x_shift;
                  int y_shift = (p == 0) ? 0 : chroma_y_shift;
      
                  av_image_copy_plane(
                      supernova->temp_data + offset,
                      in->width >> x_shift,
                      in->data[p],
                      in->linesize[p],
                      in->width >> x_shift,
                      in->height >> y_shift
                  );
      
                  offset += ((in->width >> x_shift) * (in->height >> y_shift));
              }
      
              offset = 0;
              while (offset < size) {
                  length = write(
                      supernova->in_pipe,
                      supernova->temp_data + offset,
                      size - offset
                  );
                  if (length < 0) {
                      av_log(ctx, AV_LOG_ERROR, "could not write buffer : %d", length);
                      return AVERROR_EXTERNAL;
                  } else if (length == 0) {
                      av_log(ctx, AV_LOG_INFO, "got EoF");
                      return AVERROR_EOF;
                  }
                  offset += length;
              }
      
              av_log(ctx, AV_LOG_TRACE, "frame(%ld) write done", inlink->frame_count_in);
              memset(supernova->temp_data, 0, size * sizeof(uint8_t));
      
              offset = 0;
              while (offset < size) {
                  length = read(
                      supernova->out_pipe,
                      supernova->temp_data + offset,
                      size - offset
                  );
      
                  if (length <= 0) {
                      av_log(ctx, AV_LOG_ERROR, "could not read buffer : %d", length);
                      return AVERROR_EXTERNAL;
                  } else if (length == 0) {
                      av_log(ctx, AV_LOG_INFO, "got EoF");
                      return AVERROR_EOF;
                  }
      
                  offset += length;
              }
      
              offset = 0;
              for (int p = 0; p < nb_planes; p++) {
                  int x_shift = (p == 0) ? 0 : chroma_x_shift;
                  int y_shift = (p == 0) ? 0 : chroma_y_shift;
      
                  av_image_copy_plane(
                      out->data[p],
                      out->linesize[p],
                      supernova->temp_data + offset,
                      in->width >> x_shift,
                      in->width >> x_shift,
                      in->height >> y_shift
                  );
      
                  offset += ((in->width >> x_shift) * (in->height >> y_shift));
              }
      
              av_log(ctx, AV_LOG_TRACE, "frame(%ld) read done", outlink->frame_count_in);
          }
      
          if (in != out)
              av_frame_free(&in);
      
          return ff_filter_frame(outlink, out);
      }


      AVFilter Structure

      위에 구현된 함수들을 입력 출력 AVFilterPad 구조체 및 AVFilter 구조체에 등록합니다. 이후 위의 FFmpeg Docs - Writing Filters를 참조하여 makefile 등을 수정후 빌드 하면 됩니다.

      static const AVFilterPad supernova_inputs[] = {
          {
              .name         = "default",
              .type         = AVMEDIA_TYPE_VIDEO,
              .filter_frame = filter_frame,
          },
      };
      
      static const AVFilterPad supernova_outputs[] = {
          {
              .name = "default",
              .type = AVMEDIA_TYPE_VIDEO,
              .config_props = config_props,
          },
      };
      
      const AVFilter ff_vf_supernova = {
          .name          = "supernova",
          .description   = NULL_IF_CONFIG_SMALL("SKT Supernova"),
          .priv_size     = sizeof(SupernovaContext),
          .init          = init,
          .uninit        = uninit,
          FILTER_INPUTS(supernova_inputs),
          FILTER_OUTPUTS(supernova_outputs),
          FILTER_QUERY_FUNC(query_formats),
          .priv_class    = &supernova_class,
          .flags         = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC,
      };



      다음번에는 python 부분의 코드를 확인해보도록 하겠습니다.

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      fromDK 님의 최신 블로그

      더보기
      동영상 기고하기