for 420v, camera output CVPixelBuffer, Y channel value exceed the range [16, 235]

Platfrom: iphone XR

System: ios 17.3.1

using iphone front camera(normal camera), configure data output format to 'kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange' ('420v' (video range))

I found that Cb, Cr is inside [16, 240], but Y is outside range [16, 235], e.g 240, 255

It will lead that after convert to rbg, rgb may be negative number , and then clamp the r,g,b value between 0 and 255, finally convert clamped rgb back to yuv, yuv is different from origin yuv.

The maxium difference of y channel will be 20.

Both procssing by pure cpu and using metal shader will get this result

CVPixelBuffer.h

kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange = '420v', /* Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct */


// ... some code ...
// config camra data output format 
    NSDictionary* options = @{
        (__bridge NSString*)kCVPixelBufferPixelFormatTypeKey :  @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange),
        //(__bridge NSString*)kCVPixelBufferMetalCompatibilityKey : @(YES),
    };
    [_videoDataOutput setVideoSettings:options];
// ... some code ...

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
{
     CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
     CVPixelBufferRef pixelBuffer = imageBuffer;


       CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
        uint8_t* yBase  = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
        uint8_t* uvBase = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
        
        int imageWidth  = (int)CVPixelBufferGetWidth(pixelBuffer); // 720
        int imageHeight = (int)CVPixelBufferGetHeight(pixelBuffer);// 1280
        
        int y_width   = (int)CVPixelBufferGetWidthOfPlane (pixelBuffer, 0); // 720
        int y_height  = (int)CVPixelBufferGetHeightOfPlane(pixelBuffer, 0); // 1280
        int uv_width  = (int)CVPixelBufferGetWidthOfPlane (pixelBuffer, 1); // 360
        int uv_height = (int)CVPixelBufferGetHeightOfPlane(pixelBuffer, 1); // 640
        
        int y_stride  = (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);         int uv_stride = (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1); // 768
         
        // check Y-plane 
        if (TRUE) {
            for(int i = 0 ; i < imageHeight ; i++) {
                for(int j = 0; j < imageWidth ; j++) {
                    uint8_t nv12pixel   = *(yBase    + y_stride * i + j ); 
                    if (nv12pixel < 16 || nv12pixel > 235) { // [16, 235]
                        NSLog(@"%s: y panel out of range, coord (x:%d, y:%d), h-coord (x:%d, y:%d) ; nv12 %u "
                              ,__FUNCTION__
                              ,j ,i 
                              ,j/2, i/2
                              ,nv12pixel );
                    }
                }
            }
        } 
      CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);

}

// ... some code ...


How to deal with this case ?

Hope to get reply, Thanks

Video range means that black is 16 and white is 235, then there are super black and super white, that were historically useful when you had to color correct the video and restore missing details because the image exposure was not the best one.

How are you converting from YpCbCr to RGB and back? Are you converting back to limited range or to full range.

@galad87

"there are super black and super white, that were historically useful " -> more information is available ?

"How are you converting from YpCbCr to RGB and back? " -> Yes, code as below. Because the metal texture is uint8(RGBA8), similarly I clamp the rgb result to uint8 after converting from YpCbCr to RGB while procssing by pure cpu .

"Are you converting back to limited range or to full range." -> Both from YpCbCr to RGB and from RGB to YpCbCr , the formulas are using video-range as below.

How should I handle this out-of-range situation? don't care ?


        int Y1  = 224 ;
        int Cb1 = 118 ;
        int Cr1 = 134 ;

        float R1 = 1.164 * (Y1 - 16)                       + 1.792 * (Cr1 - 128);
        float G1 = 1.164 * (Y1 - 16) - 0.213 * (Cb1 - 128) - 0.534 * (Cr1 - 128);
        float B1 = 1.164 * (Y1 - 16) + 2.114 * (Cb1 - 128);

        int Rint = (int)round(R1) ; 
        int Gint = (int)round(G1) ;
        int Bint = (int)round(B1) ;

        Rint = fmin(Rint, 255); Rint = fmax(Rint, 0);
        Gint = fmin(Gint, 255); Gint = fmax(Gint, 0);
        Bint = fmin(Bint, 255); Bint = fmax(Bint, 0);

        float Y  = 16 + 0.183 * Rint + 0.614 * Gint + 0.062 * Bint;
        float Cb =128 - 0.101 * Rint - 0.339 * Gint + 0.439 * Bint;
        float Cr =128 + 0.439 * Rint - 0.399 * Gint - 0.040 * Bint;

@galad87 Should I clamp Y between 16 and 235 before convet YCbCr to RGB , like below


Y  = clamp(Y,   16,  235) 
Cb = clamp(Cb, 16,  240 )
Cr = clamp(Cr,  16,  240)


for 420v, camera output CVPixelBuffer, Y channel value exceed the range [16, 235]
 
 
Q