動画版snowみたいなの作れない?
少し前にsnowというカメラアプリが流行りました。
このアプリはカメラの画像をリアルタイムに認識して耳を生やしたりします。
そこで、これを動画にしてリアルタイムで映像を加工したビデオチャットや生放送できれば面白いんじゃない?という話がありました。
ソースコード
参考程度に置いておきます。
このコードはそのままビルドできません。
このコード自体は画像認識処理をOpenCV3.xからGoogleのcompute vision apiに切り替えようとした時にライブラリ(protobuffer周り)のバージョン依存問題にぶち当たってそのままほったらかしです。
https://github.com/alivelime/native-webrtc-opencv-facetracker
デモ
(サーバー代がかさむので削除)
ウェブカメラを繋いでスタートボタンを押してください。
顔が検出できない場合はぼかしをかけます。
顔が検出できたら(正確には口を検出しています)、ヒゲメガネを合成します。それだけです。
デモは1対1で、自分がサーバーに送信した映像を自分でみる機能のみとなっています。(他人に見られることはありません)
手元のipad air2で動作しています。
遅延は0.5から1秒くらいです。smallインスタンスでOpenCVの検出器はデフォルトのものを使っているため制度は低いです。
仕組み
- ウェブカメラの画像をWebRTCを使ってサーバー側へ送信します。
- サーバー側でlibwebrtcを使い、動画を受け取ります。
- 1フレームずつ描画してビットマップデータを作ります。
- OpenCVに放り込んで顔検出します。
- 検出された情報に基づいて、ヒゲメガネを追加するなり、他の画像で隠すなりします。
- 加工された画像を動画してエンコードします。
- ユーザーにWebRTCで動画を送り返します。
何に使えるか?
AIとかと組み合わせたら色々できるかも?
今までは端末で処理していたので、iOS版作ったり、android版作ったり、PC版作ったり大変だよねー。
これからはWebRTCに対応したブラウザがあれば、サーバー側で処理を九州できるよやったね!
プログラム的な仕組み
ベースはlibwebrtcです。
- 基本はOnAddStreamです。 videotrackをvideorendererに登録し、加工してcaptureクラスに渡します。 そのcaptureクラスから送り返すためのストリームを作成する流れになります。
void rtc::scoped_refptr<webrtc::MediaStreamInterface> stream){
// 受信した映像を処理するための準備をします。
webrtc::VideoTrackVector tracks = stream->GetVideoTracks(); // これがウェブカメラの映像です。
std::unique_ptr<CustomVideoCapturer> capturer(new CustomVideoCapturer(...); // captureクラスを作ります。
... new VideoRenderer(1, 1, tracks[0], *capturer ); // このtracks[0]がイベントループを持っています。
// 加工したデータを送り返すためのストリームを作ります。
rtc::scoped_refptr<webrtc::VideoTrackInterface> video_track(
peer_connection_factory->CreateVideoTrack(
"video_label",
peer_connection_factory->CreateVideoSource(
std::move(capturer), NULL)
)
);
rtc::scoped_refptr<webrtc::MediaStreamInterface> new_stream =
peer_connection_factory->CreateLocalMediaStream("stream_label");
for (auto &track : stream->GetAudioTracks()) {new_stream->AddTrack(track);}
new_stream->AddTrack(video_track);
if (!peer_connection->AddStream(new_stream)) {
LOG(LS_ERROR) << "Adding stream to PeerConnection failed";
}
}
- 受信した映像を書き出すためのレンダークラスを用意します。
#include "webrtc/api/video/video_frame.h"
#include "webrtc/api/mediastreaminterface.h"
#include "libyuv/convert_argb.h"
class VideoRenderer : public rtc::VideoSinkInterface<webrtc::VideoFrame> {
public:
void OnFrame(const webrtc::VideoFrame& frame) override; // このメソッドでレンダリングします。
protected:
std::unique_ptr<uint8_t[]> image; // これに実際のビットマップデータ(RGBX8)が入ります。
rtc::scoped_refptr<webrtc::VideoTrackInterface> rendered_track; // これにデータを書き出します。
CustomVideoCapturer& capturer; // これに加工済みデータを流し込んで新しい動画を作ります。
...
}
VideoRenderer::VideoRenderer(int w, int h,
rtc::scoped_refptr<webrtc::VideoTrackInterface> track_to_render,
CustomVideoCapturer& cap)
: rendered_track(track_to_render), width(w), height(h), capturer(cap) {
rendered_track->AddOrUpdateSink(this, rtc::VideoSinkWants()); // このvideotrackがマイフレームごとに処理を行います。
}
VideoRenderer::~VideoRenderer() {
rendered_track->RemoveSink(this);
}
// videotrackが1フレーム受信するごとにこの関数を呼び出します。
void VideoRenderer::OnFrame(const webrtc::VideoFrame& video_frame) {
LOG(INFO) << "VideoRenderer::OnFrame()";
rtc::scoped_refptr<webrtc::I420BufferInterface> buffer(video_frame.video_frame_buffer()->ToI420());
SetSize(buffer->width(), buffer->height());
libyuv::I420ToARGB(buffer->DataY(), buffer->StrideY(),
buffer->DataU(), buffer->StrideU(),
buffer->DataV(), buffer->StrideV(),
image.get(),
width * 4,
buffer->width(), buffer->height());
// captureクラスで顔検出します。
capturer.Render(image.get(), width, height);
}
// ネットワークの状況によって動的にサイズが変わります。
void VideoRenderer::SetSize(int w, int h) {
LOG(INFO) << "VideoRenderer::SetSize(" << w << "," << h << ")";
if (width == w && height == h) {
return;
}
width = w;
height = h;
image.reset(new std::uint8_t[width * height * 4]);
}
- 新しい動画ストリームを作るためのキャプチャークラスを実装します。
#include "webrtc/media/base/videocapturer.h"
class CustomVideoCapturer :
public cricket::VideoCapturer
{
public:
explicit CustomVideoCapturer(const Session& session);
virtual ~CustomVideoCapturer();
// cricket::VideoCapturer implementation.
virtual cricket::CaptureState Start(const cricket::VideoFormat& capture_format) override;
virtual void Stop() override;
virtual bool IsRunning() override;
virtual bool GetPreferredFourccs(std::vector<uint32_t>* fourccs) override;
virtual bool GetBestCaptureFormat(const cricket::VideoFormat& desired, cricket::VideoFormat* best_format) override;
virtual bool IsScreencast() const override;
virtual void Render(std::uint8_t* image, const int width, const int height); // このメソッドがVideoTrackのOnFrame()から呼ばれます。
private:
std::unique_ptr<Renderer> renderer; // このクラスで顔検出や加工します。
};
void CustomVideoCapturer::Render(std::uint8_t* image, const int width, const int height) {
// フレームレートを調整するような処理はこの辺に入れる。
// 画像を描画して加工する
myRenderer->hoge(image, width, height);
// 加工した画像をエンコードする
int buf_width = renderer->GetWidth();
int buf_height = renderer->GetHeight();
// rtc::scoped_refptr<webrtc::I420Buffer> buffer = webrtc::I420Buffer::Create(width, height);
rtc::scoped_refptr<webrtc::I420Buffer> buffer = webrtc::I420Buffer::Create(
buf_width, buf_height, buf_width, (buf_width + 1) / 2, (buf_width + 1) / 2);
const int conversionResult = webrtc::ConvertToI420(
webrtc::VideoType::kARGB, renderer->Image(), 0, 0, // No cropping
buf_width, buf_height, CalcBufferSize(webrtc::VideoType::kARGB, buf_width, buf_height),
webrtc::kVideoRotation_0, buffer.get());
if (conversionResult < 0) {
LOG(LS_ERROR) << "Failed to convert capture frame from type "
<< static_cast<int>(webrtc::VideoType::kARGB) << "to I420.";
return ;
}
webrtc::VideoFrame frame(buffer, 0, rtc::TimeMillis(), webrtc::kVideoRotation_0);
frame.set_ntp_time_ms(0);
OnFrame(frame, buf_width, buf_height);
}
cricket::CaptureState CustomVideoCapturer::Start(const cricket::VideoFormat& capture_format)
{
LOG(INFO) << "CustomVideoCapture start.";
if (capture_state() == cricket::CS_RUNNING) {
LOG(LS_ERROR) << "Start called when it's already started.";
return capture_state();
}
now_rendering = true;
SetCaptureFormat(&capture_format);
return cricket::CS_RUNNING;
}
void CustomVideoCapturer::Stop()
{
LOG(INFO) << "CustomVideoCapture::Stop()";
now_rendering = false;
if (capture_state() == cricket::CS_STOPPED) {
LOG(LS_ERROR) << "Stop called when it's already stopped.";
return;
}
SetCaptureFormat(NULL);
SetCaptureState(cricket::CS_STOPPED);
}
bool CustomVideoCapturer::IsRunning()
{
return capture_state() == cricket::CS_RUNNING;
}
bool CustomVideoCapturer::GetPreferredFourccs(std::vector<uint32_t>* fourccs)
{
if (!fourccs)
return false;
fourccs->push_back(cricket::FOURCC_I420);
return true;
}
bool CustomVideoCapturer::GetBestCaptureFormat(const cricket::VideoFormat& desired, cricket::VideoFormat* best_format)
{
if (!best_format)
return false;
// Use the desired format as the best format.
best_format->width = desired.width;
best_format->height = desired.height;
best_format->fourcc = cricket::FOURCC_I420;
best_format->interval = desired.interval;
return true;
}
bool CustomVideoCapturer::IsScreencast() const
{
return false;
}
- opencvとか好きなように映像を加工する。 ここまできてやっと自由に画像をイジれるのです。1フレームあたりの処理を縮めるかフレームを読み飛ばさないと延々と遅延が拡大します。。
画像バッファは BGRA(それぞれ8ビット)になっています。
あと、どうでもいいですが、OpenCVは cv::Mat(height, width, ...) つまり高さを先に書く書き方と cv::Mat(cv::Size(width, height), ...)という書き方があって、
うっかり cv::Mat(width, height, ..) と書くとおかしな挙動になります。
class MyRenderer {
public:
hoge(std::uint8_t* image, int w, int h);
protected:
cv::Mat buffer;
}
void MyRenderer::hoge(std::uint8_t* image, int w, int h) {
buffer = cv::Mat(cv::Size(width, height), CV_8UC4, image);
// 検出器
cv::CascadeClassifier detector;
detector.load("haarcascade_frontalface_alt2.xml");
detector.detectMultiScale(target, results, scaleFactor, minNeighbors, 0,
cv::Size(width * 0.1, height * 0.1));
// resultsにデータが入っているので何か処理をする。
}
- ビルド さて、プログラムを書いた後はビルドをしないといけないのですが、また一手間必要です。 libwebrtcは速度を重視するため、-fno-exceptionsなどの例外処理を削っていたりします。 なのでwebrtc系とopencv系はヘッダーを一緒にインクルードしないようにして別々のコンパイルスイッチで処理してからリンクすると良いです。
cflags_opencv = -std=c++11 -fPIC -Wexpansion-to-defined -O0 --sysroot=/opt/webrtc/src/build/linux/debian_jessie_amd64-sysroot
ライブラリはこれを追加しましょう。
ldflags = ... -lopencv_core -lopencv_imgproc -lopencv_imgcodecs -lopencv_objdetect