Logo
Advanced Video Processing Pipeline

Video Privacy Shield: Face Tracking, Masking & Re-encoding

Build a complete C++ solution using FFmpeg 7 and OpenCV to detect faces, overlay transparent elements (PNG stickers), and re-encode the pixels back into a hardware-compatible MP4 container.

Step 1: Define Project Toolchains

Introduce encoder modules and format muxers for FFmpeg 7

CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(VideoFaceMasker)

set(CMAKE_CXX_STANDARD 17)

# Load OpenCV
find_package(OpenCV REQUIRED)

# Load FFmpeg 7 components (including encoder & muxer libraries)
find_package(PkgConfig REQUIRED)
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET 
    libavcodec 
    libavformat 
    libavutil 
    libswscale
    libavfilter
)

add_executable(VideoFaceMasker main.cpp)

target_link_libraries(VideoFaceMasker 
    PRIVATE 
    ${OpenCV_LIBS}
    PkgConfig::FFMPEG
)

Step 2: Full C++ Processing & Encoding Pipeline

Bi-directional conversion contexts with explicit flush handling

Crucial Color Space Notice: OpenCV cascades and PNG asset blending must be operated inside BGR24 color space. However, standard MP4/H.264 video requires YUV420P. Two SwsContext instances are used simultaneously to ensure strict compatibility.
main.cpp
#include <iostream>
#include <vector>
#include <string>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
}

#include <opencv2/opencv.hpp>
#include <opencv2/objdetect.hpp>

// Helper function: Blends a 4-channel PNG sticker onto a 3-channel BGR base image
void overlaySticker(cv::Mat& baseImg, const cv::Mat& sticker, const cv::Rect& faceRect) {
    if (sticker.empty()) return;
    
    // Scale sticker to match the exact size of detected face boundary
    cv::Mat resizedSticker;
    cv::resize(sticker, resizedSticker, faceRect.size());

    for (int y = 0; y < faceRect.height; ++y) {
        for (int x = 0; x < faceRect.width; ++x) {
            int base_y = faceRect.y + y;
            int base_x = faceRect.x + x;

            // Boundary check for safety
            if (base_y >= baseImg.rows || base_x >= baseImg.cols || base_y < 0 || base_x < 0)
                continue;

            cv::Vec4b stickerPixel = resizedSticker.at<cv::Vec4b>(y, x);
            float alpha = stickerPixel[3] / 255.0f; // Extract Alpha Channel weight

            if (alpha > 0.1f) { 
                cv::Vec3b& basePixel = baseImg.at<cv::Vec3b>(base_y, base_x);
                // Alpha blending formula
                for (int c = 0; c < 3; ++c) {
                    basePixel[c] = static_cast<uchar>((1.0f - alpha) * basePixel[c] + alpha * stickerPixel[c]);
                }
            }
        }
    }
}

// Helper function: Performs the actual encoding frame pipeline block
bool encodeAndWriteFrame(AVCodecContext* encCtx, AVFormatContext* outFormatCtx, AVStream* outStream, AVFrame* frame, AVPacket* encPacket) {
    int ret = avcodec_send_frame(encCtx, frame);
    if (ret < 0) return false;

    while (ret >= 0) {
        ret = avcodec_receive_packet(encCtx, encPacket);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return true;
        } else if (ret < 0) {
            return false;
        }

        // Re-scale timestamps from encoder timebase to muxer stream timebase
        av_packet_rescale_ts(encPacket, encCtx->time_base, outStream->time_base);
        encPacket->stream_index = outStream->index;

        av_interleaved_write_frame(outFormatCtx, encPacket);
        av_packet_unref(encPacket);
    }
    return true;
}

int main(int argc, char** argv) {
    if (argc < 5) {
        std::cerr << "Usage: ./VideoFaceMasker <input_video> <output_video> <cascade_xml> <sticker_png>" << std::endl;
        return -1;
    }

    std::string inputPath = argv[1];
    std::string outputPath = argv[2];
    std::string cascadePath = argv[3];
    std::string stickerPath = argv[4];

    // Load OpenCV cascade model and the sticker asset (with alpha channel)
    cv::CascadeClassifier faceCascade;
    if (!faceCascade.load(cascadePath)) {
        std::cerr << "Error: Failed to load cascade XML." << std::endl;
        return -1;
    }
    cv::Mat stickerImg = cv::imread(stickerPath, cv::IMREAD_UNCHANGED);
    if (stickerImg.empty() || stickerImg.channels() != 4) {
        std::cerr << "Error: Sticker must be a valid 4-channel PNG image." << std::endl;
        return -1;
    }

    // ─── 1. 初始化输入解码器管线 ───
    AVFormatContext* inFormatCtx = nullptr;
    if (avformat_open_input(&inFormatCtx, inputPath.c_str(), nullptr, nullptr) != 0) return -1;
    if (avformat_find_stream_info(inFormatCtx, nullptr) < 0) return -1;

    int videoStreamIdx = -1;
    for (unsigned int i = 0; i < inFormatCtx->nb_streams; i++) {
        if (inFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStreamIdx = i;
            break;
        }
    }
    if (videoStreamIdx == -1) return -1;

    const AVCodec* decoder = avcodec_find_decoder(inFormatCtx->streams[videoStreamIdx]->codecpar->codec_id);
    AVCodecContext* decCtx = avcodec_alloc_context3(decoder);
    avcodec_parameters_to_context(decCtx, inFormatCtx->streams[videoStreamIdx]->codecpar);
    if (avcodec_open2(decCtx, decoder, nullptr) < 0) return -1;

    // ─── 2. 初始化输出编码器管线 (FFmpeg 7 标准) ───
    AVFormatContext* outFormatCtx = nullptr;
    avformat_alloc_output_context2(&outFormatCtx, nullptr, nullptr, outputPath.c_str());
    if (!outFormatCtx) return -1;

    const AVCodec* encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
    AVStream* outStream = avformat_new_stream(outFormatCtx, nullptr);
    AVCodecContext* encCtx = avcodec_alloc_context3(encoder);

    // Configure hardware compression constraints
    encCtx->height = decCtx->height;
    encCtx->width = decCtx->width;
    encCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    // Setup framerate metadata metrics
    encCtx->time_base = av_inv_q(av_guess_frame_rate(inFormatCtx, inFormatCtx->streams[videoStreamIdx], nullptr));
    outStream->time_base = encCtx->time_base;

    if (outFormatCtx->oformat->flags & AVFMT_GLOBALHEADER) {
        encCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }
    if (avcodec_open2(encCtx, encoder, nullptr) < 0) return -1;
    avcodec_parameters_from_context(outStream->codecpar, encCtx);

    // Open output disk file IO handler
    if (!(outFormatCtx->oformat->flags & AVFMT_NOFILE)) {
        if (avio_open(&outFormatCtx->pb, outputPath.c_str(), AVIO_FLAG_WRITE) < 0) return -1;
    }
    if (avformat_write_header(outFormatCtx, nullptr) < 0) return -1;

    // ─── 3. 像素缓冲区与像素色彩变换转换器配置 ───
    AVFrame* inFrame = av_frame_alloc();
    AVFrame* bgrFrame = av_frame_alloc();
    AVFrame* outFrame = av_frame_alloc();
    AVPacket* inPacket = av_packet_alloc();
    AVPacket* outPacket = av_packet_alloc();

    outFrame->width = encCtx->width;
    outFrame->height = encCtx->height;
    outFrame->format = encCtx->pix_fmt;
    av_frame_get_buffer(outFrame, 0);

    // Converter A: YUV to BGR24
    int bgrBufferSize = av_image_get_buffer_size(AV_PIX_FMT_BGR24, decCtx->width, decCtx->height, 1);
    uint8_t* bgrBuffer = (uint8_t*)av_malloc(bgrBufferSize);
    av_image_fill_arrays(bgrFrame->data, bgrFrame->linesize, bgrBuffer, AV_PIX_FMT_BGR24, decCtx->width, decCtx->height, 1);
    SwsContext* decSwsCtx = sws_getContext(
        decCtx->width, decCtx->height, decCtx->pix_fmt,
        decCtx->width, decCtx->height, AV_PIX_FMT_BGR24,
        SWS_BILINEAR, nullptr, nullptr, nullptr
    );

    // Converter B: BGR24 back to YUV420P
    SwsContext* encSwsCtx = sws_getContext(
        decCtx->width, decCtx->height, AV_PIX_FMT_BGR24,
        encCtx->width, encCtx->height, encCtx->pix_fmt,
        SWS_BILINEAR, nullptr, nullptr, nullptr
    );

    int frameCounter = 0;

    // ─── 4. 主循环处理链路 ───
    while (av_read_frame(inFormatCtx, inPacket) >= 0) {
        if (inPacket->stream_index == videoStreamIdx) {
            if (avcodec_send_packet(decCtx, inPacket) == 0) {
                while (avcodec_receive_frame(decCtx, inFrame) == 0) {
                    
                    // Execute Step A conversion
                    sws_scale(decSwsCtx, inFrame->data, inFrame->linesize, 0, decCtx->height, bgrFrame->data, bgrFrame->linesize);

                    // Direct mapping to OpenCV container
                    cv::Mat frameMat(decCtx->height, decCtx->width, CV_8UC3, bgrFrame->data[0], bgrFrame->linesize[0]);

                    // Face identification cascade phase
                    cv::Mat gray;
                    cv::cvtColor(frameMat, gray, cv::COLOR_BGR2GRAY);
                    cv::equalizeHist(gray, gray);
                    std::vector<cv::Rect> faces;
                    faceCascade.detectMultiScale(gray, faces, 1.1, 3, 0, cv::Size(40, 40));

                    // Iterate and overlay stickers onto targeted face geometries
                    for (const auto& face : faces) {
                        overlaySticker(frameMat, stickerImg, face);
                    }

                    // Execute Step B conversion back to YUV
                    av_frame_make_writable(outFrame);
                    sws_scale(encSwsCtx, bgrFrame->data, bgrFrame->linesize, 0, decCtx->height, outFrame->data, outFrame->linesize);
                    
                    // Sync Presentation Timestamps (PTS) directly
                    outFrame->pts = inFrame->pts;

                    // Send the modified frame to encoding module
                    encodeAndWriteFrame(encCtx, outFormatCtx, outStream, outFrame, outPacket);
                    frameCounter++;
                }
            }
        }
        av_packet_unref(inPacket);
    }

    // ─── 5. 解码与编码延迟包冲刷清理段 (Flush) ───
    // Flush encoder cached queues
    encodeAndWriteFrame(encCtx, outFormatCtx, outStream, nullptr, outPacket);
    av_write_trailer(outFormatCtx);

    std::cout << "\n[Process Completed] " << frameCounter << " Frames written successfully." << std::endl;

    // ─── 6. 销毁环境物理内存释放 ───
    av_free(bgrBuffer);
    av_frame_free(&inFrame);
    av_frame_free(&bgrFrame);
    av_frame_free(&outFrame);
    av_packet_free(&inPacket);
    av_packet_free(&outPacket);
    avcodec_free_context(&decCtx);
    avcodec_free_context(&encCtx);
    avformat_close_input(&inFormatCtx);
    if (!(outFormatCtx->oformat->flags & AVFMT_NOFILE)) {
        avio_closep(&outFormatCtx->pb);
    }
    avformat_free_context(outFormatCtx);
    sws_freeContext(decSwsCtx);
    sws_freeContext(encSwsCtx);

    return 0;
}

Step 3: Verification & CLI Execution

Validate output compatibility using standard media tools

1. Compile & Execution Arguments

Ensure the fourth parameter points to a valid 32-bit RGBA PNG file to enable perfect transparency overlay.

mkdir build && cd build
cmake .. && make
CLI Execution Template:
./VideoFaceMasker ./raw.mp4 ./masked_out.mp4 ./haarcascade_frontalface_default.xml ./glasses_sticker.png
2. Pipeline Status Metrics

Upon successful completion, the console reports total processed frames matching original metadata durations.

Expected Console Output:
[OpenCV] Cascade loaded successfully.
[FFmpeg] Output file initialized: H.264 / AVC codec active.
...
[ Process Completed ] 450 Frames written successfully.