ProRes 4444 blocky compression artifacts

I’m creating a objective C command-line utility to encode RAW image sequences to ProRes 4444, but I’m encountering, blocky compression artifacts in the ProRes 4444 video output.

To test the integrity of the image data before encoding to ProRes, I added a snippet in my encoding function that saves a 16-bit PNG before encoding to ProRes and the PNG looks perfect, I can see all detail in every part of the image dynamic range.

Here’s a comparison between the 16-bit PNG(on the right) and the ProRes 4444 output. (on the left)

As a further test, I re-encoded the ‘test PNG’ to ProRes 4444 using DaVinci Resolve, and the ProRes4444 output video from Resolve doesn’t have any blocky compression artifacts. Looks identical.

In short, this is what the utility does:

  1. Unpacks the 12-bit raw data into 16-bit values. After unpacking, the raw data is debayered to convert it into a standard color image format (BGR) using OpenCV.

  2. Scale the debayered pixel values from their original 12-bit depth to fit into a 16-bit range. Up to this point everything is fine and confirmed by saving 16bit PNGs.

  3. The images are encoded to ProRes 4444 using the AVFoundation framework.

The pixel buffers are created and managed using dictionary method with ‘kCVPixelFormatType_64RGBALE’.

I need help figuring this out, I’m a real novice when it comes to AVfoundation/encoding to ProRes.

See relevant parts of my 'encodeToProRes' function:

void encodeToProRes(const std::string &outputPath, const std::vector<std::string> &rawPaths, const std::string &proResFlavor) {
    NSError *error = nil;
    NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
    AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeQuickTimeMovie error:&error];
    if (error) {
        std::cerr << "Error creating AVAssetWriter: " << error.localizedDescription.UTF8String << std::endl;
        return;
    }

    // Load the first image to get the dimensions
    std::cout << "Debayering the first image to get dimensions..." << std::endl;
    Mat firstImage;
    int width = 5320;
    int height = 3900;
    if (!debayer_image(rawPaths[0], firstImage, width, height)) {
        std::cerr << "Error debayering the first image" << std::endl;
        return;
    }
    width = firstImage.cols;
    height = firstImage.rows;

    // Save the first frame as a PNG 16-bit image for validation
    std::string pngFilePath = outputPath + "_frame1.png";
    if (!imwrite(pngFilePath, firstImage)) {
        std::cerr << "Error: Failed to save the first frame as a PNG image" << std::endl;
    } else {
        std::cout << "First frame saved as PNG: " << pngFilePath << std::endl;
    }
    
    
    NSString *codecKey = nil;
    if (proResFlavor == "4444") {
        codecKey = AVVideoCodecTypeAppleProRes4444;
    } else if (proResFlavor == "422HQ") {
        codecKey = AVVideoCodecTypeAppleProRes422HQ;
    } else if (proResFlavor == "422") {
        codecKey = AVVideoCodecTypeAppleProRes422;
    } else if (proResFlavor == "LT") {
        codecKey = AVVideoCodecTypeAppleProRes422LT;
    } else {
        std::cerr << "Error: Invalid ProRes flavor specified: " << proResFlavor << std::endl;
        return;
    }

    NSDictionary *outputSettings = @{
        AVVideoCodecKey: codecKey,
        AVVideoWidthKey: @(width),
        AVVideoHeightKey: @(height)
    };

    AVAssetWriterInput *videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:outputSettings];
    videoInput.expectsMediaDataInRealTime = YES;

    NSDictionary *pixelBufferAttributes = @{
        (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_64RGBALE),
        (id)kCVPixelBufferWidthKey: @(width),
        (id)kCVPixelBufferHeightKey: @(height)
    };

    AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoInput sourcePixelBufferAttributes:pixelBufferAttributes];

...

    [assetWriter startSessionAtSourceTime:kCMTimeZero];

    CMTime frameDuration = CMTimeMake(1, 24); // Frame rate of 24 fps
    int numFrames = static_cast<int>(rawPaths.size());

...

    // Encoding thread
    std::thread encoderThread([&]() {
        int frameIndex = 0;
        std::vector<CVPixelBufferRef> pixelBufferBuffer;

        while (frameIndex < numFrames) {
            std::unique_lock<std::mutex> lock(queueMutex);
            queueCondVar.wait(lock, [&]() { return !frameQueue.empty() || debayeringFinished; });

            if (!frameQueue.empty()) {
                auto [index, debayeredImage] = frameQueue.front();
                frameQueue.pop();
                lock.unlock();

                if (index == frameIndex) {

                    cv::Mat rgbaImage;
                    cv::cvtColor(debayeredImage, rgbaImage, cv::COLOR_BGR2RGBA);

                    CVPixelBufferRef pixelBuffer = NULL;
                    CVReturn result = CVPixelBufferPoolCreatePixelBuffer(NULL, adaptor.pixelBufferPool, &pixelBuffer);
                    if (result != kCVReturnSuccess) {
                        std::cerr << "Error: Could not create pixel buffer" << std::endl;
                        dispatch_group_leave(dispatchGroup);
                        return;
                    }

                    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
                    void *pxdata = CVPixelBufferGetBaseAddress(pixelBuffer);
                    for (int row = 0; row < height; ++row) {
                        memcpy(static_cast<uint8_t*>(pxdata) + row * CVPixelBufferGetBytesPerRow(pixelBuffer),
                               rgbaImage.ptr(row),
                               width * 8);
                    }
                    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);

                    pixelBufferBuffer.push_back(pixelBuffer);

...

Thanks very much!

Answered by Robino in 791981022

I figured it out.. basically because my Raw images are linear gamma, the low end is very dark and the encoder doesn't have a lot to work with so I added a log transformation prior to encoding and that did the trick, no more blocky compression artifacts.

Accepted Answer

I figured it out.. basically because my Raw images are linear gamma, the low end is very dark and the encoder doesn't have a lot to work with so I added a log transformation prior to encoding and that did the trick, no more blocky compression artifacts.

ProRes 4444 blocky compression artifacts
 
 
Q