MediaPipe 파헤치기 (2)
포스트
취소

MediaPipe 파헤치기 (2)

해당 글은 본인이 공부하면서 파악하기 위해서 작성한 글입니다. 잘못된 정보나 추가적인 정보가 들어가야 한다면 댓글로 알려주시면 감사하겠습니다!

코드 수정에 앞서서 MediaPipe를 구성하고 있는 요소들이 무엇인지에 대해서 살펴봅니다.

MediaPipe 홈페이지에서 설명하는 프레임워크의 컨셉은 크게 아래와 같이 6개로 설명할 수 있으며, 실질적으로 구성하고 있는것은 Graphs, Calculators, Packets 입니다.

  • Calculators
  • Graphs
  • Packets
  • Synchronization
  • GPU
  • Real-time Streams

Graph는 전체 Pipeline의 구조를 의미하고 노드로 이루어져있습니다. 또한 *.pbtxt파일을 통해서 저장됩니다.

Claculators는 각 노드로서 데이터를 변화가거나 사용해서 다시 데이터를 내보냅니다.

Packet은 노드 사이에 전달되는 데이터와 같은 것들을 말합니다. 이 Packet이 통하는 calculator 사이의 길을 stream 이라고 합니다.

자세한 내용은 여기를 참고해 주세요.

그럼 다시 되돌아가서 이전에 빌드하였던 코드를 다시 살펴보겠습니다.

1
$ bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 mediapipe/examples/desktop/hand_tracking:hand_tracking_cpu

여기서 target은 mediapipe/examples/desktop/hand_tracking:hand_tracking_cpu로 되어있습니다.

앞부분의 mediapipe/examples/desktop/hand_tracking은 target의 path를 말하고,
뒷부분의 hand_tracking_cpu 은 실제 target을 말합니다.

실제 해당 위치의 디렉터리를 확인해보면 BUILD 파일이 위치한것을 볼 수 있습니다. 해당 BUILD 파일의 내용은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# Copyright 2019 The MediaPipe Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

licenses(["notice"])

package(default_visibility = ["//mediapipe/examples:__subpackages__"])

cc_binary(
    name = "hand_tracking_tflite",
    deps = [
        "//mediapipe/examples/desktop:simple_run_graph_main",
        "//mediapipe/graphs/hand_tracking:desktop_tflite_calculators",
    ],
)

cc_binary(
    name = "hand_tracking_cpu",
    deps = [
        "//mediapipe/examples/desktop:demo_run_graph_main",
        "//mediapipe/graphs/hand_tracking:desktop_tflite_calculators",
    ],
)

# Linux only
cc_binary(
    name = "hand_tracking_gpu",
    deps = [
        "//mediapipe/examples/desktop:demo_run_graph_main_gpu",
        "//mediapipe/graphs/hand_tracking:mobile_calculators",
    ],

좀전에 위에서 target으로 :hand_tracking_cpu를 지정해주었습니다.

위의 내용을 보면 name = "hand_tracking_cpu"인 cc_binary가 있습니다. 따라서 위의 bazel 빌드 코드는해당 cc_binary를 빌드하겠다는 의미입니다.

좀더 들어가서 아래의 두 파일을 살펴봅니다.

  • //mediapipe/examples/desktop:demo_run_graph_main
  • //mediapipe/graphs/hand_tracking:desktop_tflite_calculators

/mediapipe/examples/desktop:demo_run_graph_main.cc파일을 먼저 살펴보겠습니다.

가장 아래쪽에 위치하고 있는 main()을 먼저 살펴봅니다.

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char** argv) {
  google::InitGoogleLogging(argv[0]);
  absl::ParseCommandLine(argc, argv);
  absl::Status run_status = RunMPPGraph();
  if (!run_status.ok()) {
    LOG(ERROR) << "Failed to run the graph: " << run_status.message();
    return EXIT_FAILURE;
  } else {
    LOG(INFO) << "Success!";
  }
  return EXIT_SUCCESS;
}

예상을 해보면 RunMPPGraph()을 통해서 그래프를 실행하고 결과 상태가 ok이면 성공, 아니면 실패로 볼 수 있을것 같습니다.

RunMPPGraph()가 무엇을 하는걸까요…? 그래도 하나씩 살펴봅니다…

1
2
absl::Status RunMPPGraph() {
  std::string calculator_graph_config_contents;
1
2
3
MP_RETURN_IF_ERROR(mediapipe::file::GetContents(
    absl::GetFlag(FLAGS_calculator_graph_config_file),
    &calculator_graph_config_contents));

MP_RETURN_IF_ERROR# define MP_RETURN_IF_ERROR검색해서 찾을 수 있었습니다.

1
2
// Evaluates an expression that produces a `absl::**Status**`. If the status
// is not ok, returns it from the current function.

GetContents 같은 경우는 calculator_graph_config_contentscalculator_graph_config_file의 데이터를 넣어주는것 같습니다.(참고1, 참고2)

다음과 같이 정의 되고 있습니다.

1
absl::Status GetContents(absl::string_view file_name, std::string* output, bool read_as_binary)

이제 GetFlag를 살펴 봅니다. 찾아보니 C++라이브러리(링크) 였습니다. ABSL_FLAG를 통해서 flag를 정의한다고 합니다. (참고1, 참고2)

FLAGS_calculator_graph_config_file를 가만보니 위쪽에 정의가 되어있었습니다.

1
ABSL_FLAG(std::string, calculator_graph_config_file, "", "Name of file containing text format CalculatorGraphConfig proto.");

자 이제 빌드한 bazel을 실행할때 코드를 떠올려 봅니다.

1
2
GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/hand_tracking/hand_tracking_cpu \
  --calculator_graph_config_file=mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxt

calculator_graph_config_file를 통해서 Graph 이름을 불러오고 calculator_graph_config_contents에 해당 정보를 넣어줍니다.

이제야 어느정도 실마리가 보입니다ㅠㅠ 지금 2시간 넘게 파악하는데 글을 쓰면서 하다보니 이제 2줄 파악했네요..

다음 코드를 순차적으로 봅시당.

1
2
LOG(INFO) << "Get calculator graph config contents: "
          << calculator_graph_config_contents;

로그는 슥 건너가 줍니다…..

1
2
3
mediapipe::CalculatorGraphConfig config =
    mediapipe::ParseTextProtoOrDie<mediapipe::CalculatorGraphConfig>(
        calculator_graph_config_contents);

내용을 보니 그래프의 정보를 calculator_graph_config_contents 를 읽어서 가져온다는것 같습니다. (참고)

여기을 보면 proto 파일 형식에 대해서 알 수 있고 graph의 구조인 pbtxt는 proto 파일 형식을 따르고 있다는것을 알 수 있습니다.

또한 graph의 구조는 여기에서 확인할 수 있습니다.

1
LOG(INFO) << "Initialize the calculator graph.";
1
2
mediapipe::CalculatorGraph graph;
MP_RETURN_IF_ERROR(graph.Initialize(config));

Initialize의 코드는 여기서 찾았습니다. 설명을 찾는데 너무 오래걸렸습니다..

1
 // Convenience version which does not take side packets.
1
LOG(INFO) << "Initialize the camera or load the video.";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  cv::VideoCapture capture;
  const bool load_video = !absl::GetFlag(FLAGS_input_video_path).empty();
  if (load_video) {
    capture.open(absl::GetFlag(FLAGS_input_video_path));
  } else {
    capture.open(0);
  }
  RET_CHECK(capture.isOpened());

  cv::VideoWriter writer;
  const bool save_video = !absl::GetFlag(FLAGS_output_video_path).empty();
  if (!save_video) {
    cv::namedWindow(kWindowName, /*flags=WINDOW_AUTOSIZE*/ 1);
#if (CV_MAJOR_VERSION >= 3) && (CV_MINOR_VERSION >= 2)
    capture.set(cv::CAP_PROP_FRAME_WIDTH, 640);
    capture.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
    capture.set(cv::CAP_PROP_FPS, 30);
#endif
  }

자… 이제 본격적으로 cv가 나오기 시작했습니다.. 내용을 보니 웹캠을 열어주는 코드입니다.
빌드파일을 실행시 발생했던 오류는 RET_CHECK(capture.isOpened()); 여기서 발생하는듯 보입니다.

비디오 저장 여부를 확인해주고 비디오를 출력할 창을 kWindowName라는 이름으로 열어줍니다.

1
LOG(INFO) << "Start running the calculator graph.";
1
2
3
ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller poller,
                graph.AddOutputStreamPoller(kOutputStream));
MP_RETURN_IF_ERROR(graph.StartRun({}));

ASSIGN_OR_RETURN설명은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Executes an expression `rexpr` that returns a `absl::StatusOr<T>`. On
// OK, extracts its value into the variable defined by `lhs`, otherwise returns
// from the current function. By default the error status is returned
// unchanged, but it may be modified by an `error_expression`. If there is an
// error, `lhs` is not evaluated; thus any side effects that `lhs` may have
// only occur in the success case.
//
// Interface:
//
//   ASSIGN_OR_RETURN(lhs, rexpr)
//   ASSIGN_OR_RETURN(lhs, rexpr, error_expression);
//
// WARNING: expands into multiple statements; it cannot be used in a single
// statement (e.g. as the body of an if statement without {})!
//
// Example: Declaring and initializing a new variable (ValueType can be anything
//          that can be initialized with assignment, including references):
//   ASSIGN_OR_RETURN(ValueType value, MaybeGetValue(arg));
//
// Example: Assigning to an existing variable:
//   ValueType value;
//   ASSIGN_OR_RETURN(value, MaybeGetValue(arg));
//
// Example: Assigning to an expression with side effects:
//   MyProto data;
//   ASSIGN_OR_RETURN(*data.mutable_str(), MaybeGetValue(arg));
//   // No field "str" is added on error.
//
// Example: Assigning to a std::unique_ptr.
//   ASSIGN_OR_RETURN(std::unique_ptr<T> ptr, MaybeGetPtr(arg));
//
// If passed, the `error_expression` is evaluated to produce the return
// value. The expression may reference any variable visible in scope, as
// well as a `mediapipe::StatusBuilder` object populated with the error and
// named by a single underscore `_`. The expression typically uses the
// builder to modify the status and is returned directly in manner similar
// to MP_RETURN_IF_ERROR. The expression may, however, evaluate to any type
// returnable by the function, including (void). For example:
//
// Example: Adjusting the error message.
//   ASSIGN_OR_RETURN(ValueType value, MaybeGetValue(query),
//                    _ << "while processing query " << query.DebugString());
//
// Example: Logging the error on failure.
//   ASSIGN_OR_RETURN(ValueType value, MaybeGetValue(query), _.LogError());

결국 graph.AddOutputStreamPoller(kOutputStream)의 status가 OK가 나오면 mediapipe::OutputStreamPoller poller에 해당 값을 추출하겠다는것 같습니다.

graph.StartRun()은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
absl::Status CalculatorGraph::StartRun(
    const std::map<std::string, Packet>& extra_side_packets,
    const std::map<std::string, Packet>& stream_headers) {
  RET_CHECK(initialized_).SetNoLogging()
      << "CalculatorGraph is not initialized.";
  MP_RETURN_IF_ERROR(PrepareForRun(extra_side_packets, stream_headers));
  MP_RETURN_IF_ERROR(profiler_->Start(executors_[""].get()));
  scheduler_.Start();
  return absl::OkStatus();
}

파악이 어렵지만. 대략적으로 그래프가 initialize 되어있나 확인하고 실행하는것 같습니다. 아래쪽에 scheduler도 시작하는것을 볼 수 있습니다.

자… 이제 outputStream도 연결이 되었고, 그래프도 동작하기 시작했습니다. 이제 다음 코드를 살펴봅니다..

1
LOG(INFO) << "Start grabbing and processing frames.";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  bool grab_frames = true;
  while (grab_frames) {
    // Capture opencv camera or video frame.
    cv::Mat camera_frame_raw;
    capture >> camera_frame_raw;
    if (camera_frame_raw.empty()) {
      if (!load_video) {
        LOG(INFO) << "Ignore empty frames from camera.";
        continue;
      }
      LOG(INFO) << "Empty frame, end of video reached.";
      break;
    }
    cv::Mat camera_frame;
    cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB);
    if (!load_video) {
      cv::flip(camera_frame, camera_frame, /*flipcode=HORIZONTAL*/ 1);
    }

이제 본격적으로 카메라에서 프래임을 가져오기 시작합니다. 영상정보에 대한 예외 처리를 해주고 BGR로 되어있는 영상을 RGB로 바꿔줍니다. ​ 다음으로는 수평하게 프레임을 뒤집어주는데 흔히 셀카 좌우 뒤집기를 수행해서 왼손을 올리면 화면에서도 왼쪽이 올라갈 수 있도록 처리한것 같습니다.

1
2
3
4
    // Wrap Mat into an ImageFrame.
    auto input_frame = absl::make_unique<mediapipe::ImageFrame>(
        mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows,
        mediapipe::ImageFrame::kDefaultAlignmentBoundary);

absl::make_unique에 대한 설명이 있지만 잘 이해는 안갑니다.

좀더 찾아보니 C++14 이후부터 제공되는 make_unique 함수는 unique_ptr 인스턴스을 안전하게 생성하는 방법이라고 합니다.(참고) C++을 모르는 상태로 코드를 파악중인데 C++ 공부가 많이 필요해 보입니다.

결국 mediapipe::ImageFrame에 해당하는 unique_ptr 인스턴스인 input_frame을 생성하고 있습니다. mediapipe::ImageFrame이 무엇이냐! 하면 여기에 나와있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// A container for storing an image or a video frame, in one of several
// formats.  Pixels are encoded row-major in an interleaved fashion.
//
// Formats supported by ImageFrame are listed in the ImageFormat proto.
// It is the intention of ImageFormat to specify both the data format
// and the colorspace used.  For example GRAY8 and GRAY16 both use the
// same colorspace but have different formats.  Although it would be
// possible to keep HSV, linearRGB, or BGR values inside an ImageFrame
// (with format SRGB) this is an abuse of the class.  If you need a new
// format, please add one to ImageFormat::Format.
//
// Do not assume that the pixel data is stored contiguously.  It may be
// stored with row padding for alignment purposes.

결국 이미지나 비디오의 프래임을 담고있는 컨테이너 라고 볼 수 있겠습니다.

1
2
    cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get());
    camera_frame.copyTo(input_frame_mat);

결국 카메라 영상과 같은 형태의 matrix(Mat)을 설정해두고 여기에 copyTo를 이용해서 영상에서 순간 frame을 가져오는 것 같습니다. 여기에서 힌트를 얻었습니다.

1
2
3
    // Send image packet into the graph.
    size_t frame_timestamp_us =
        (double)cv::getTickCount() / (double)cv::getTickFrequency() * 1e6;

사용할 timestamp를 만들어줍니다.

1
2
3
    MP_RETURN_IF_ERROR(graph.AddPacketToInputStream(
        kInputStream, mediapipe::Adopt(input_frame.release())
                          .At(mediapipe::Timestamp(frame_timestamp_us))));

해당 스크립트의 위쪽을 보면 KInputStream, KOutputStream이 정의된것을 볼 수 있습니다.

1
2
constexpr char kInputStream[] = "input_video";
constexpr char kOutputStream[] = "output_video";

이제 Adopt를 살펴보면.,

1
2
3
// Returns a Packet that adopts the object; the Packet assumes the ownership.
// The timestamp of the returned Packet is Timestamp::Unset(). To set the
// timestamp, the caller should do Adopt(...).At(...).

그럼 가져오는 object가 뭔지 보기 위해서 imageframe의 release()를 찾아봅니다.

1
2
// Relinquishes ownership of the pixel data.  Notice that the unique_ptr
// uses a non-standard deleter.

이제 대략적으로 보면 *.h 에 정의와 설명이 되어있고 같은 이름의 *.cc 에 실제 동작이 정의되어있군요.. 한번더 C++ 공부를 해야 겠다는 생각을 합니다.

보면 픽셀데이터의 소유권을 양도해준다는 것 같습니다. 결과적으로 보면 input_frame의 픽셀데이터를 받아서 KInputStream에 넣어주겠다 이말이군요..

1
2
3
4
    // Get the graph result packet, or stop if that fails.
    mediapipe::Packet packet;
    if (!poller.Next(&packet)) break;
    auto& output_frame = packet.Get<mediapipe::ImageFrame>();

앞서 poller는 mediapipe::OutputStreamPoller입니다. poller.Next(&packet)의 의미는 poller의 다음페킷을 packet에다가 넣어주겠다는 뜻입니다. (참고)

결국 여기서 Input, OutputStreamPoller가 연결된 하나의 그래프의 출력이 나오는거죠!? 이 packet은 mediapipe::ImageFrame 타입으로 마지막줄처럼 packet에서 데이터를 가져옵니다.

(이제 또다른 문제가 발생했습니다.. Graph의 출력이 mediapipe::ImageFrame이 되도록 하는 코드를 찾아야 겠죠!)

그다음 받은 데이터를 output_frame으로 받아줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    // Convert back to opencv for display or saving.
    cv::Mat output_frame_mat = mediapipe::formats::MatView(&output_frame);
    cv::cvtColor(output_frame_mat, output_frame_mat, cv::COLOR_RGB2BGR);
    if (save_video) {
      if (!writer.isOpened()) {
        LOG(INFO) << "Prepare video writer.";
        writer.open(absl::GetFlag(FLAGS_output_video_path),
                    mediapipe::fourcc('a', 'v', 'c', '1'),  // .mp4
                    capture.get(cv::CAP_PROP_FPS), output_frame_mat.size());
        RET_CHECK(writer.isOpened());
      }
      writer.write(output_frame_mat);
    } else {
      cv::imshow(kWindowName, output_frame_mat);
      // Press any key to exit.
      const int pressed_key = cv::waitKey(5);
      if (pressed_key >= 0 && pressed_key != 255) grab_frames = false;
    }
  }

이제 출력 결과를 받았으니 이걸 표시하거나 저장해줍니다.

1
  LOG(INFO) << "Shutting down.";
1
2
3
4
  if (writer.isOpened()) writer.release();
  MP_RETURN_IF_ERROR(graph.CloseInputStream(kInputStream));
  return graph.WaitUntilDone();
}

이제 그래프의 InputStream을 닫아주고 graph를 종료합니다.

그런데 마지막이 너무 입력 -> 출력 이라서 과연 내부에서 모델이 어떻게 implement 되고 inference를 수행하며 출력된 결과물을 이미지에 표시하는지에 대한 정보가 없습니다.

이를 파악하기 위해선 다음 포스팅에서 GRAPH에 대해서 좀더 살펴 봐야 할것 같습니다.

포스팅의 앞부분에서 언급했던 //mediapipe/graphs/hand_tracking:desktop_tflite_calculators도 다음 포스팅에서 확인을 해보겠습니다!

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

MediaPipe 파헤치기 (1)

MediaPipe 파헤치기 (3)