缩放 NV12 C/C++
前言
由于 ffmpeg4.4 缩放 NV12 后会导致颜色出现异常, 最后只能通过理解 NV12 的数据排列方式来实现 NV12 的缩放.
调用 FFmpeg 的 sws_getContext (NV12→NV12颜色异常)
测试了其中的所有缩放算法, 但是缩放后的图像都会有问题, 颜色对不上, 原始大佬知道原因
NV12→NV12 是异常的,
但是 NV12→RGB24→NV12 是 OK 的
但是使用命令行调用倒是正常的(就是没能在ffmpeg.c中找到对应的 NV12 缩放, 目前推测是内部算法没有实现 NV12 的缩放)
ffmpeg -f rawvideo -pix_fmt nv12 -s 1920x1080 -i input.nv12 -vf scale=1920:1080 -c:v rawvideo -pix_fmt nv12 -f rawvideo out.nv12
// #define SWS_FAST_BILINEAR 1
// #define SWS_BILINEAR 2
// #define SWS_BICUBIC 4
// #define SWS_X 8
// #define SWS_POINT 0x10
// #define SWS_AREA 0x20
// #define SWS_BICUBLIN 0x40
// #define SWS_GAUSS 0x80
// #define SWS_SINC 0x100
// #define SWS_LANCZOS 0x200
// #define SWS_SPLINE 0x400
sws = sws_getContext(srcw, srch, src_fmt, dstw, dsth, dst_fmt, SWS_BILINEAR,
nullptr, nullptr, nullptr);
手写一个 NV12 缩放算法
了解 NV12 的存储方式
NV12:
每 4 个像素点为一组 (2 x 2), 这里一共 16 个像素点
Y0 Y1 Y2 Y3
Y4 Y5 Y6 Y7
Y8 Y9 Y10 Y11
Y12 Y13 Y14 Y15
---------------
U0 V0 U2 V2
U4 V4 U6 V6
这里有 4 组像素点, 每一组 4 个像素点
Y0 Y1 U0 V0 -> Y0U0V0 + Y1U0V0 + Y4U0V0 + Y5U0V0
Y2 Y3 Y6 Y7 -> Y2U2V2 + Y3U2V2 + Y6U2V2 + Y7U2V2
Y8 Y9 Y12 Y13 -> Y8U4V4 + Y9U4V4 + Y10U4V4 + Y11U4V4
Y10 Y11 Y14 Y15 -> Y10U6V6 + Y11U6V6 + Y14U6V6 + Y15U6V6
如何缩放:
由于 UV 是交错存储的, 所以这里让 UV 作为一组, 每次复制像素点以此为单位(4 个 像素点为单位),
而不是以一个像素点作为单位.
核心算法见 scaleNV12(char *dst, const char *kSrc, const int kSrcW,
const int kSrcH, const int kDstW, const int kDstH)
算法实现
#include "scalenv12.h"
#include <QtDebug>
ScaleNV12::ScaleNV12(QObject *parent) : QObject{parent} {}
// 核心算法
void ScaleNV12::scaleNV12(char *dst, const char *kSrc, const int kSrcW,
const int kSrcH, const int kDstW, const int kDstH) {
const double kScaleW = 1.0 * kSrcW / kDstW;
const double kScaleH = 1.0 * kSrcH / kDstH;
const int kDstYSize = kDstW * kDstH;
const int KSrcYSize = kSrcW * kSrcH;
for (int dsty = 0; dsty < kDstH; ++dsty) {
int srcy = static_cast<int>(dsty * kScaleH);
int dst_start = kDstW * dsty;
int src_start = srcy * kSrcW;
for (int dstx = 0; dstx < kDstW - 1; dstx += 2) {
int srcx = static_cast<int>(dstx * kScaleW);
// 向下取整, 保证在同一行中, 是以两个 Y 作为一组
if ((srcx & 1) != 0) srcx--;
memcpy(dst + dst_start + dstx, kSrc + src_start + srcx, 2);
}
}
for (int dsty = 0; dsty < kDstH / 2; ++dsty) {
int srcy = static_cast<int>(dsty * kScaleH);
int dst_start = kDstW * dsty + kDstYSize;
int src_start = srcy * kSrcW + KSrcYSize;
// 确保 UV 分量不被拆分
for (int dstx = 0; dstx < kDstW - 1; dstx += 2) {
int srcx = static_cast<int>(dstx * kScaleW);
// 向下取整, 保证在同一行中, 是以 UV 作为一组
if ((srcx & 1) != 0) srcx--;
memcpy(dst + dst_start + dstx, kSrc + src_start + srcx, 2);
}
}
}
//测试调用
bool ScaleNV12::scale(const QString &kInput, const QString &kOutput,
double scale) {
bool ret = false;
FILE *in = fopen(kInput.toUtf8().constData(), "rb");
if (in == nullptr) {
qDebug() << "failed: fopen" << kInput;
return false;
}
FILE *out = fopen(kOutput.toUtf8().constData(), "wb");
if (out == nullptr) {
fclose(in);
qDebug() << "failed: fopen" << kOutput;
return false;
}
int srcw = 1920;
int srch = 1080;
int dstw = 1920 * scale;
int dsth = 1080 * scale;
int frame_index = 0;
const int src_size = srcw * srch * 3 / 2;
const int dst_size = srcw * scale * srch * scale * 3 / 2;
QString result_info{};
char *in_buffer{};
char *out_buffer{};
in_buffer = new char[src_size];
out_buffer = new char[dst_size];
while (!feof(in) && frame_index++ < 60) {
fread(in_buffer, 1, src_size, in);
scaleNV12(out_buffer, in_buffer, srcw, srch, dstw, dsth);
fwrite(out_buffer, 1, dst_size, out);
// qDebug() << __func__ << "write frame index:" << frame_index;
}
ret = true;
qDebug() << "scale finised, you can use the following command to play "
"the test";
result_info = QString::asprintf(
"ffplay -f rawvideo -pixel_format %s -video_size %dx%d "
"-framerate %d %s",
"nv12", dstw, dsth, 25, kOutput.toUtf8().constData());
// system(result_info.toUtf8().constData());
qDebug() << result_info;
fclose(in);
fclose(out);
if (in_buffer) {
delete[] in_buffer;
}
if (out_buffer) {
delete[] out_buffer;
}
return ret;
}
//调用演示
#include <QCoreApplication>
#include "scalenv12.h"
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
ScaleNV12 scale;
const QString input_path = "/home/share/samba/60s.nv12";
// const QString input_path = "/home/share/samba/60s.rgb24";
// const QString input_path = "/home/share/samba/60s.yuv420p";
const QString output_path = "/home/share/samba/out.nv12";
scale.scale(input_path, output_path, 1.5);
scale.scale(input_path, output_path, 0.8);
QMetaObject::invokeMethod(&a, "quit", Qt::QueuedConnection);
return a.exec();
}