ROS講座122 mjpeg_streamを受信する

# 環境
この記事は以下の環境で動いています。

| 項目 | 値 |
|:-:|:-:|
| CPU | Core i5-8250U |
| Ubuntu | 18.04 |
| ROS | Melodic |
| Gazebo | 9.0.0 |
| python | 2.7.17 |
| OpenCV | 3.2.0|
| Qt | 5.5.1 |

インストールについては[ROS講座02 インストール](https://qiita.com/srs/items/e0e0a9dc3f94c2d3348e)を参照してください。
またこの記事のプログラムはgithubにアップロードされています。[ROS講座11 gitリポジトリ](https://qiita.com/srs/items/25a56a6abda41dcf396e)を参照してください。

# 概要
以前（[62 Webカメラの画像をブラウザから見る](https://qiita.com/srs/items/7f86c56d0005184dcbc0)）にsensor_msgs/Imageの画像をwebブラウザで見るということをしました。今回はこれをc++のプログラムから見ていきましょう。

## 動画の形式
動画の配信形式はいろいろありますが、`web-video-server`ではmotion-jpeg(mjpeg)という形式で配信します。またこの形式のストリームはOpenCVを使って受信することが出来ます。

# OpenCVで受信して表示する
## ソースコード(cpp)

```cpp:ros_lecture/app_lecture/src/mjpeg_opencv.cpp
#include <unistd.h>
#include <memory>
#include <iostream>
#include <thread>
#include <functional>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

class MjpegView
{
public:
  MjpegView(std::string target, std::string topic)
  {
    std::string uri = "http://" + target + "/stream?topic=" + topic;
    if (!vcap_.open(uri))
    {
      std::cout << "Error opening video stream or file" << std::endl;
      return;
    }
    cv::namedWindow("Output Window");
    recv_thread_ = std::thread(std::bind(&MjpegView::loop, this));
  }

private:
  void loop(void)
  {
    while (true)
    {
      cv::Mat image;
      if (!vcap_.read(image))
      {
        std::cout << "No frame" << std::endl;
        return;
      }
      cv::imshow("Output Window", image);
      cv::waitKey(1);
    }
  }

  cv::VideoCapture vcap_;
  std::thread recv_thread_;
};

int main(int, char**)
{
  MjpegView mjpeg_view("localhost:8080", "/camera/image_raw");
  while (true)
  {
    sleep(1);
  }
}
```

* `cv::VideoCapture`がmjpeg sreamを受信するクラスです。`open()`で引数はmjpeg streamへのuriです。webブラウザでみえるurlを入れとけばアクセスします。
* `read()`関数で受信したmjpegのフレームを取得します。この関数は取得するまでブロックします。

## ソースコード(CMake) 

```cmake:ros_lecture/app_lecture/CMakeLists.txt
find_library(PTHREAD_LIBRARY pthread) # この行を追加
find_package(OpenCV REQUIRED) # この行を追加

include_directories(
# include
  ${catkin_INCLUDE_DIRS}
  ${OpenCV_INCLUDE_DIRS} # この行を追加
  Simple-WebSocket-Server
)

add_executable(mjpeg_opencv src/mjpeg_opencv.cpp) # この行を追加

target_link_libraries(mjpeg_opencv
  ${PTHREAD_LIBRARY} # この行を追加
  ${OpenCV_LIBRARIES} # この行を追加
)
```

* OpenCVへの依存を追加しました。
* もしcatkinのリンクをしない場合はpthreadへのリンクが必要です。

## ビルド
```shell
cd ~/catkin_ws
catkin_make
```

## 実行

```shell:1つ目のターミナル
roslaunch cam_lecture sim_only.launch
```

```shell:2つ目のターミナル
rosrun web_video_server web_video_server 
```

```shell:3つ目のターミナル
rosrun app_lecture mjpeg_opencv
```

以下のようなOpenCVの画面が出ます。
![app_mjpeg1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/116522c4-f6b1-3c82-6d5b-7818bc333300.png)

# OpenCVで受信してQtで表示する。
QtのほうがOpenCVよりもほかの要素と組み合わせてGUIを作るのに向いています。OpenCVで受信して、それをQtの形式に変換してQtで表示してみます。

## ソースコード(cpp)
```cpp:ros_lecture/app_lecture/src/mjpeg_qt.cpp
#include <future>
#include <unistd.h>
#include <memory>
#include <iostream>
#include <thread>
#include <functional>
#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <QApplication>
#include <QLabel>
#include <QImage>

class MjpegView
{
public:
  MjpegView(std::string target, std::string topic)
  {
    std::string uri = "http://" + target + "/stream?topic=" + topic;
    if (!vcap_.open(uri))
    {
      std::cout << "Error opening video stream or file" << std::endl;
      return;
    }
    label_ = new QLabel("######## basic1 ########");
    label_->show();
    recv_thread_ = std::thread(std::bind(&MjpegView::loop, this));
  }

private:
  void loop(void)
  {
    while (true)
    {
      cv::Mat image;
      if (!vcap_.read(image))
      {
        std::cout << "No frame" << std::endl;
        return;
      }
      cv::Mat rgb_image;
      cv::cvtColor(image, rgb_image, cv::COLOR_BGR2RGB);
      QImage qimage(rgb_image.data, rgb_image.cols, rgb_image.rows, QImage::QImage::Format_RGB888);
      label_->resize(rgb_image.cols, rgb_image.rows);
      label_->setPixmap(QPixmap::fromImage(qimage));
    }
  }

  cv::VideoCapture vcap_;
  QLabel* label_;
  std::thread recv_thread_;
  std::thread qt_thread_;
};

int main(int argc, char** argv)
{
  QApplication app(argc, argv);
  MjpegView mjpeg_view("localhost:8080", "/camera/image_raw");
  return app.exec();
}
```

* Qtでは画像を表示するWidgetというものはありません。何らかのWidgetにpixmapを割り当てるという方法で画像を表示します。今回はQLabelを使います。
* OpenCVのcv::Mat形式からQtのQImage形式に変換する必要があります。まず色の順番をBGR->RGBに並べ替えます。その後でQImageの形式に変えます。
* setPixmap(QPixmap::fromImage(qimage))で特定のWidgetに画像をセットします。

## ソースコード(CMake) 

```
set(CMAKE_AUTOMOC ON)
find_package(Qt5 ${rviz_QT_VERSION} EXACT REQUIRED
  Core
  Widgets
)
set(QT_LIBRARIES Qt5::Widgets)
add_definitions(-DQT_NO_KEYWORDS)

add_executable(mjpeg_qt src/mjpeg_qt.cpp)

target_link_libraries(mjpeg_qt
  ${PTHREAD_LIBRARY}
  ${OpenCV_LIBRARIES} 
  ${QT_LIBRARIES}
)
```

Qtをビルドする設定を加えています。

## ビルド
```shell
cd ~/catkin_ws
catkin_make
```

## 実行

```shell:1つ目のターミナル
roslaunch cam_lecture sim_only.launch
```

```shell:2つ目のターミナル
rosrun web_video_server web_video_server 
```

```shell:3つ目のターミナル
rosrun app_lecture mjpeg_qt
```

以下のようなQtの画面が出ます。
![app_mjpeg2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/d3a9c5cb-51ba-fa36-0104-ce81f1842f2f.png)

# 参考
[OpenCVでmjpgを受信](https://seinzumtode.hatenadiary.jp/entry/20160712/1468317007)
[Opencv->Qtの変換](https://symfoware.blog.fc2.com/blog-entry-2302.html)

# 目次ページへのリンク
[ROS講座の目次へのリンク](https://qiita.com/srs/items/5f44440afea0eb616b4a)