You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无法正确转换YUV420P至RGBA的技术求助(Delphi+FFmpeg)

问题:YUV420P转RGBA后画面颜色失真(面部呈蓝色)

基于Delphi VCL和DirectSound开发视频播放器,跟随libav教程实现YUV420P帧转RGBA帧时,画面颜色严重失真(如人物面部呈现蓝色)。尝试使用sws_setColorspaceDetails修复未成功,更换不同格式视频文件(.mp4/.mkv)后问题依然存在。使用的libav头文件、二进制文件均为常用开源版本,测试视频为标准BigBuckBunny片段。

unit uVideoPlayer;

interface

uses
  Winapi.Windows, Winapi.Messages,
  System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms,
  Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
  libavcodec, libavdevice, libavfilter,
  libavformat, libavutil, libswscale;

type
  TfrmMain = class(TForm)
    btnPlay: TButton;
    edtFile: TEdit;
    mem: TMemo;
    img: TImage;
    procedure FormCreate(Sender: TObject);
    procedure btnPlayClick(Sender: TObject);
  end;

var
  frmMain: TfrmMain;

implementation

uses
  Uutils,
  USmoothResize,
  System.AnsiStrings;

{$R *.dfm}

function DecodePacket(pPacket: PAVPacket; pCodecCtx: PAVCodecContext;
  pSwsCtx: PSWSContext; pFrame, pFrameRGB: PAVFrame): Integer;
var
  vResponse: Integer;
  vSrcBmp, vDstBmp: TBitmap;
begin
  vResponse := avcodec_send_packet(pCodecCtx, pPacket);

  if vResponse < 0 then
    Exit(vResponse);

  while (vResponse >= 0) do
  begin
    vResponse := avcodec_receive_frame(pCodecCtx, pFrame);
    if (vResponse = AVERROR_EAGAIN) or (vResponse = AVERROR_EOF) then
      Break
    else if vResponse < 0 then
      Exit(vResponse);

    if vResponse >= 0 then
    begin
      frmMain.mem.Lines.Add(Format('Frame %d (type=%s, size=%d bytes, format=%d) pts %d key_frame %d [DTS %d]',
      [
        pCodecCtx.frame_number,
        av_get_picture_type_char(pFrame.pict_type),
        pFrame.pkt_size,
        pFrame.format,
        pFrame.pts,
        pFrame.key_frame,
        pFrame.coded_picture_number
      ]));

      if (pFrame.format <> Integer(AV_PIX_FMT_YUV420P)) then
        frmMain.mem.Lines.Add('Not a grayscale image!');

      pFrame.data[0] := pFrame.data[0] + pFrame.linesize[0] * (pCodecCtx.height - 1);
      pFrame.linesize[0] := pFrame.linesize[0] * -1;
      pFrame.data[1] := pFrame.data[1] + pFrame.linesize[1] * (pCodecCtx.height div 2 - 1);
      pFrame.linesize[1] := pFrame.linesize[1] * -1;
      pFrame.data[2] := pFrame.data[2] + pFrame.linesize[2] * (pCodecCtx.height div 2 - 1);
      pFrame.linesize[2] := pFrame.linesize[2] * -1;

      sws_scale(pSwsCtx, @pFrame^.Data, @pFrame^.Linesize, 0,
        pCodecCtx.height, @pFrameRGB.data[0], @pFrameRGB.Linesize[0]);

      vSrcBmp := TBitmap.Create;
      vDstBmp := nil;
      try
        vSrcBmp.Height := pCodecCtx.height;
        vSrcBmp.Width := pCodecCtx.width;
        vSrcBmp.PixelFormat := TPixelFormat.pf32bit;

        MoveMemory(vSrcBmp.ScanLine[vSrcBmp.Height -1], pFrameRGB.data[0],
          pFrameRGB.linesize[0] * pCodecCtx.Height);

        vDstBmp := TBitmap.Create;
        vDstBmp.Width := frmMain.img.Width;
        vDstBmp.Height := frmMain.img.Height;
        vDstBmp.PixelFormat := TPixelFormat.pf32bit;

        SmoothResize(vSrcBmp, vDstBmp);

        frmMain.img.Picture.Assign(vDstBmp);

        Application.ProcessMessages;
      finally
        FreeAndNil(vSrcBmp);
        FreeAndNil(vDstBmp);
      end;
    end;
  end;
  Exit(0);
end;

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  libavformat.av_register_all;
end;

procedure TfrmMain.btnPlayClick(Sender: TObject);
type
  TQuadInt = array [0..3] of Integer;
  PQuadInt = ^TQuadInt;
var
  vCount, vVideoStreamIndex, vDecPackResponse: Integer;
  vFileName: AnsiString;
//  vI1, vI2: PQuadInt;

  vFormatCtx: PAVFormatContext;
  vCodec, vLocalCodec: PAVCodec;
  vCodecParams, vLocalCodecParams: PAVCodecParameters;
  vCodecCtx: PAVCodecContext;
  vFrame, vFrameRGB: PAVFrame;
  vPacket: PAVPacket;
  vImgConvertCtx: PSWSContext;
begin
  vFormatCtx := avformat_alloc_context;

  if vFormatCtx <> nil then
    mem.Lines.Add('Alloc format context succesful!')
  else
    Exit;

  vFileName := AnsiString(edtFile.Text);

  if avformat_open_input(vFormatCtx, PAnsiChar(vFileName), nil, nil) = 0 then
    mem.Lines.Add('Open input success!')
  else
    Exit;

  if avformat_find_stream_info(vFormatCtx, nil) = 0 then
    mem.Lines.Add('Find stream info AVFormat success!')
  else
    Exit;

  vCodec := nil;
  vCodecParams := nil;
  vVideoStreamIndex := -1;

  for vCount := 0 to vFormatCtx.nb_streams - 1 do
  begin
    vLocalCodecParams := vFormatCtx.streams[vCount].codecpar;

    mem.Lines.Add(Format('[%d] AVStream.time_base before open codec %d/%d',
      [vCount, vFormatCtx.streams[vCount].time_base.num, vFormatCtx.streams[vCount].time_base.den]));
    mem.Lines.Add(Format('[%d] AVStream.r_frame_rate before open codec %d/%d',
      [vCount, vFormatCtx.streams[vCount].r_frame_rate.num, vFormatCtx.streams[vCount].r_frame_rate.den]));
    mem.Lines.Add(Format('[%d] AVStream.start_time %" PRId64',
      [vCount, vFormatCtx.streams[vCount].start_time]));
    mem.Lines.Add(Format('[%d] AVStream.duration %" PRId64',
      [vCount, vFormatCtx.streams[vCount].duration]));

    vLocalCodec := avcodec_find_decoder(vLocalCodecParams.codec_id);

    if vLocalCodec = nil then
      Continue;

    if vLocalCodecParams.codec_type = AVMEDIA_TYPE_VIDEO then
    begin
      vVideoStreamIndex := vCount;

      vCodec := vLocalCodec;
      vCodecParams := vLocalCodecParams;

      mem.Lines.Add(Format('[%d] Video codec params %dx%d',
        [vCount, vLocalCodecParams.width, vLocalCodecParams.height]));
    end;
    mem.Lines.Add(Format('[%d] CodecName: %s | CodecID: %d | BitRate: %d',
      [vCount, vLocalCodec.Name, Integer(vLocalCodec.ID), vLocalCodecParams.bit_rate]));
  end;

  if vVideoStreamIndex = -1 then
    Exit;

  vCodecCtx := avcodec_alloc_context3(vCodec);
  if vCodecCtx <> nil then
    mem.Lines.Add('Codec context allocation succesful!')
  else
    Exit;

  if avcodec_parameters_to_context(vCodecCtx, vCodecParams) = 0 then
    mem.Lines.Add('Params to Ctx succesful!')
  else
    Exit;

  if avcodec_open2(vCodecCtx, vCodec, nil) = 0 then
    mem.Lines.Add('AvCodec open succesful!')
  else
    Exit;

  vFrame := av_frame_alloc;
  if (vFrame = nil) then
    Exit;

  vFrameRGB := av_frame_alloc;
  if (vFrameRGB = nil) then
    Exit;

  vFrameRGB.format := Integer(AV_PIX_FMT_RGBA);
  vFrameRGB.width  := vCodecCtx.width;
  vFrameRGB.height := vCodecCtx.height;
  if (av_frame_get_buffer( vFrameRGB, 32 )) <> 0 then
    Exit;

  vImgConvertCtx := sws_alloc_context();

  av_opt_set_int(vImgConvertCtx, 'sws_flags', SWS_POINT, 0);
  av_opt_set_int(vImgConvertCtx, 'srcw', vCodecCtx.Width, 0);
  av_opt_set_int(vImgConvertCtx, 'srch', vCodecCtx.Height, 0);
  av_opt_set_int(vImgConvertCtx, 'src_format', Integer(vCodecCtx.pix_fmt), 0);
  av_opt_set_int(vImgConvertCtx, 'dstw', vCodecCtx.Width, 0);
  av_opt_set_int(vImgConvertCtx, 'dsth', vCodecCtx.Height, 0);
  av_opt_set_int(vImgConvertCtx, 'dst_format', Integer(AV_PIX_FMT_RGBA), 0);

//  vI1 := Pointer(sws_getCoefficients(Integer(0)));
//  vI2 := Pointer(sws_getCoefficients(Integer(AVCOL_SPC_BT709)));
//  sws_setColorspaceDetails(vImgConvertCtx, Pointer(@vI1), 1, Pointer(@vI2), 1,
//    0, 1 shl 16, 1 shl 16);

  sws_init_context(vImgConvertCtx, nil, nil);

  vPacket := av_packet_alloc;
  if (vPacket = nil) then
    Exit;

  while (av_read_frame(vFormatCtx, vPacket) >= 0) do
  begin
    if vPacket.stream_index = vVideoStreamIndex then
    begin
      vDecPackResponse := DecodePacket(vPacket, vCodecCtx,
        vImgConvertCtx, vFrame, vFrameRGB);

      if vDecPackResponse < 0 then
        Break;

    end;
    av_frame_unref(vFrame);
    av_packet_unref(vPacket);
  end;

  av_frame_unref(vFrameRGB);
  av_frame_free(vFrameRGB);
  avformat_close_input(vFormatCtx);

  av_packet_free(vPacket);
  av_frame_free(vFrame);
  avcodec_free_context(vCodecCtx);
  sws_freeContext(vImgConvertCtx);
end;

end.

解决方案

1. 修正像素格式字节顺序

Delphi的TPixelFormat.pf32bit采用BGRA字节顺序(蓝-绿-红-透明度),而你当前使用的AV_PIX_FMT_RGBA是红-绿-蓝-透明度,通道顺序错位直接导致颜色失真(面部蓝色就是R和B通道颠倒的典型表现)。

修改步骤:

  • 将vFrameRGB.format改为Integer(AV_PIX_FMT_BGRA)
  • 调整SwsContext的目标格式为AV_PIX_FMT_BGRA

2. 简化SwsContext创建(避免手动配置错误)

放弃sws_alloc_context+av_opt_set_int的手动配置方式,改用sws_getContext自动生成上下文,该函数会自动处理色彩空间、尺寸等参数,可靠性更高:

替换原有的SwsContext创建代码:

// 移除以下代码块
vImgConvertCtx := sws_alloc_context();
av_opt_set_int(vImgConvertCtx, 'sws_flags', SWS_POINT, 0);
av_opt_set_int(vImgConvertCtx, 'srcw', vCodecCtx.Width, 0);
av_opt_set_int(vImgConvertCtx, 'srch', vCodecCtx.Height, 0);
av_opt_set_int(vImgConvertCtx, 'src_format', Integer(vCodecCtx.pix_fmt), 0);
av_opt_set_int(vImgConvertCtx, 'dstw', vCodecCtx.Width, 0);
av_opt_set_int(vImgConvertCtx, 'dsth', vCodecCtx.Height, 0);
av_opt_set_int(vImgConvertCtx, 'dst_format', Integer(AV_PIX_FMT_RGBA), 0);
sws_init_context(vImgConvertCtx, nil, nil);

// 替换为:
vImgConvertCtx := sws_getContext(
  vCodecCtx.Width, vCodecCtx.Height, vCodecCtx.pix_fmt,
  vCodecCtx.Width, vCodecCtx.Height, AV_PIX_FMT_BGRA,
  SWS_BILINEAR, nil, nil, nil
);
if not Assigned(vImgConvertCtx) then
begin
  mem.Lines.Add('Failed to create SwsContext');
  Exit;
end;

3. 避免修改原始Frame数据指针

你当前代码中修改了pFrame.data和pFrame.linesize的正负值来翻转画面,这种操作会破坏原始帧的结构,可能导致sws_scale处理异常。建议保留原始帧方向,在Bitmap层面处理翻转:

移除DecodePacket中的以下代码块:

pFrame.data[0] := pFrame.data[0] + pFrame.linesize[0] * (pCodecCtx.height - 1);
pFrame.linesize[0] := pFrame.linesize[0] * -1;
pFrame.data[1] := pFrame.data[1] + pFrame.linesize[1] * (pCodecCtx.height div 2 - 1);
pFrame.linesize[1] := pFrame.linesize[1] * -1;
pFrame.data[2] := pFrame.data[2] + pFrame.linesize[2] * (pCodecCtx.height div 2 - 1);
pFrame.linesize[2] := pFrame.linesize[2] * -1;

改为在Bitmap赋值时处理翻转:

// 替换原MoveMemory代码为逐行复制
var
  i: Integer;
  SrcPtr, DstPtr: PByte;
begin
  for i := 0 to vSrcBmp.Height - 1 do
  begin
    SrcPtr := pFrameRGB.data[0] + pFrameRGB.linesize[0] * (vSrcBmp.Height - 1 - i);
    DstPtr := vSrcBmp.ScanLine[i];
    MoveMemory(DstPtr, SrcPtr, vSrcBmp.Width * 4); // 4字节 per BGRA pixel
  end;

4. 确保色彩空间正确转换(可选)

如果仍存在色彩偏差,需明确指定源视频的色彩空间参数。可以从解码后的帧中获取色彩空间,传递给sws_context:

var
  srcColorSpace: Integer;
begin
  srcColorSpace := av_frame_get_colorspace(pFrame);
  sws_setColorspaceDetails(vImgConvertCtx,
    sws_getCoefficients(srcColorSpace),
    av_frame_get_color_range(pFrame),
    sws_getCoefficients(AVCOL_SPC_BT709), // 目标色彩空间(匹配显示器)
    AVCOL_RANGE_JPEG,
    0, 1 shl 16, 1 shl 16
  );

内容的提问来源于stack exchange,提问作者Claaaudio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 07:27:27