缩放 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();
}
posted @ 2023-08-28 17:05  阿风小子  阅读(160)  评论(0编辑  收藏  举报