如何用C++封装视频文件的H.264码流

How to encapsulate the H.264 bitstream of video file in C++

我正在尝试将视频文件 (.mp4) 转换为 Dicom 文件。
我已经通过在 Dicom 中存储单个图像(视频的每一帧一个)成功地做到了,
但是结果是一个太大的文件,这对我来说不是很好。
相反,我想将存储在视频文件中的 H.264 比特流封装到 Dicom 文件中。
我试图按如下方式获取文件的字节:

std::ifstream inFile(file_name, std::ifstream::binary);

inFile.seekg(0, inFile.end);
std::streampos length = inFile.tellg();
inFile.seekg(0, inFile.beg);

std::vector<unsigned char> bytes(length);

inFile.read((char*)&bytes[0], length);

但我想我错过了一些东西,比如为读取的字节封装,因为结果 Dicom 文件是一个黑色图像。

在 python 中,我会为此目的使用 pydicom.encaps.encapsulate 函数:
https://pydicom.github.io/pydicom/dev/reference/generated/pydicom.encaps.encapsulate.html

with open(videofile, 'rb') as f:
    dataset.PixelData = encapsulate([f.read()])

C++中有什么等价于encapsulate函数的吗?
或者以任何不同的方式在一个流中而不是逐帧获取视频的封装像素数据?

这是初始化Dcmdataset的代码,使用提取的bytes

VideoFileStream* vfs = new VideoFileStream();
vfs->setFilename(file_name);
if (!vfs->open())
    return false;

DcmDataset* dataset = new DcmDataset();
dataset->putAndInsertOFStringArray(DCM_SeriesInstanceUID, dcmGenerateUniqueIdentifier(new char[100], SITE_SERIES_UID_ROOT));
dataset->putAndInsertOFStringArray(DCM_SOPInstanceUID, dcmGenerateUniqueIdentifier(new char[100], SITE_INSTANCE_UID_ROOT));
dataset->putAndInsertOFStringArray(DCM_StudyInstanceUID, dcmGenerateUniqueIdentifier(new char[100], SITE_STUDY_UID_ROOT));
dataset->putAndInsertOFStringArray(DCM_MediaStorageSOPInstanceUID, dcmGenerateUniqueIdentifier(new char[100], SITE_UID_ROOT));
dataset->putAndInsertString(DCM_MediaStorageSOPClassUID, UID_VideoPhotographicImageStorage);
dataset->putAndInsertString(DCM_SOPClassUID, UID_VideoPhotographicImageStorage);
dataset->putAndInsertOFStringArray(DCM_TransferSyntaxUID, UID_MPEG4HighProfileLevel4_1TransferSyntax);
dataset->putAndInsertOFStringArray(DCM_PatientID, "987655");
dataset->putAndInsertOFStringArray(DCM_StudyDate, "20050509");
dataset->putAndInsertOFStringArray(DCM_Modality, "ES");
dataset->putAndInsertOFStringArray(DCM_PhotometricInterpretation, "YBR_PARTIAL_420");
dataset->putAndInsertUint16(DCM_SamplesPerPixel, 3);
dataset->putAndInsertUint16(DCM_BitsAllocated, 8);
dataset->putAndInsertUint16(DCM_BitsStored, 8);
dataset->putAndInsertUint16(DCM_HighBit, 7);
dataset->putAndInsertUint16(DCM_Rows, vfs->height());
dataset->putAndInsertUint16(DCM_Columns, vfs->width());
dataset->putAndInsertUint16(DCM_CineRate, vfs->framerate());
dataset->putAndInsertUint16(DCM_FrameTime, 1000.0 * 1 / vfs->framerate());
const Uint16* arr = new Uint16[]{ 0x18,0x00, 0x63, 0x10 };  
dataset->putAndInsertUint16Array(DCM_FrameIncrementPointer, arr, 4);
dataset->putAndInsertString(DCM_NumberOfFrames, std::to_string(vfs->numFrames()).c_str());
dataset->putAndInsertOFStringArray(DCM_FrameOfReferenceUID, dcmGenerateUniqueIdentifier(new char[100], SITE_UID_ROOT));
dataset->putAndInsertUint16(DCM_PixelRepresentation, 0);
dataset->putAndInsertUint16(DCM_PlanarConfiguration, 0);
dataset->putAndInsertOFStringArray(DCM_ImageType, "ORIGINAL");
dataset->putAndInsertOFStringArray(DCM_LossyImageCompression, "01");
dataset->putAndInsertOFStringArray(DCM_LossyImageCompressionMethod, "ISO_14496_10");
dataset->putAndInsertUint16(DCM_LossyImageCompressionRatio, 30);
dataset->putAndInsertUint8Array(DCM_PixelData, (const Uint8 *)bytes.data(), length);

DJ_RPLossy repParam;
dataset->chooseRepresentation(EXS_MPEG4HighProfileLevel4_1, &repParam);
dataset->updateOriginalXfer();

DcmFileFormat fileformat(dataset); 
OFCondition status = fileformat.saveFile("C://temp//videoTest", EXS_LittleEndianExplicit);

诀窍是将属性 PixelData 的值重定向到文件流。这样,视频将按需分块加载(即访问属性时)。 但是你必须明确地创建整个结构,即:

  • 像素数据元素
  • 具有...的像素序列
  • ...偏移量 table
  • ...包含 MPEG 文件内容的单个项目

代码

// set length to the size of the video file
DcmInputFileStream dcmFileStream(videofile.c_str(), 0);
DcmPixelSequence* pixelSequence = new DcmPixelSequence(DCM_PixelSequenceTag));
DcmPixelItem* offsetTable = new DcmPixelItem(DCM_PixelItemTag);
pixelSequence->insert(offsetTable);
DcmPixelItem* frame = new DcmPixelItem(DCM_PixelItemTag);
frame->createValueFromTempFile(dcmFileStream.newFactory(), OFstatic_cast(Uint32, length), EBO_LittleEndian);
pixelSequence->insert(frame);
DcmPixelData* pixelData = new DcmPixeldata(DCM_PixelData);
pixelData->putOriginalRepresentation(EXS_MPEG4HighProfileLevel4_1, nullptr, pixelSequence);
dataset->insert(pixelData, true);
DcmFileFormat fileformat(dataset); 
OFCondition status = fileformat.saveFile("C://temp//videoTest");

请注意,如果您将文件保存在 VR 隐式小字节序中,则会“破坏”压缩。

如上所述并且在代码中很明显,整个 MPEG 文件被包装到 PixelData 中的单个项目中。这是符合 DICOM 标准的,但您可能希望将单个帧分别封装在一个项目中。

注意:此处没有错误处理