Package limn.video

Class VideoColor

java.lang.Object
limn.video.VideoColor

public final class VideoColor extends Object
How the samples of a VideoFrame are to be turned into colour: which luma/chroma matrix was used to encode them, whether the codes cover the full range or the studio range, and what transfer function the encoded values carry. Layout is a separate question and lives on PixelFormat: the same I420 buffer is BT.601 or BT.709 depending only on this.

Immutable and interned: one instance per matrix, range and transfer, plus unspecified(), and nothing else can be constructed. Reference comparison is therefore exact and cheap, and the coefficient accessors are the one place the decode matrix exists; a CPU converter and a GPU shader that both read them cannot drift apart.

The coefficient accessors give the folded, ready-to-use form for a picture of a given bit depth: codes in [0..maxCode] in, RGB in [0..maxCode] out, as


 int neutral = chromaNeutral(depth);
 double y = yScale(depth) * (Y - yOffset(depth));
 double r = y + crToR(depth) * (Cr - neutral);
 double g = y + (cbToG(depth) * (Cb - neutral) + crToG(depth) * (Cr - neutral));
 double b = y + cbToB(depth) * (Cb - neutral);
 
Green's two chroma terms are summed before the luma is added, as bracketed: adding them one at a time re-associates the arithmetic and rounds some codes to a different integer, so a consumer that regroups them stops matching every other consumer of this table.

Every accessor takes the bit depth and none of them assumes eight. Studio black, the chroma neutral and the studio gain all move with the depth, so a 10-bit picture decoded through the 8-bit table is not slightly wrong; it is four times too bright with black four times too high. The argument exists so that omitting it is a compile error rather than a picture.

The chroma neutral is an integer code, never a normalized 0.5; sampled as a normalized value it is 128/255 at eight bits, and using 0.5 instead tints neutral grey by about a code.

Any thread: every instance is immutable and every accessor is a field read or one multiply.

  • Field Details

    • BT601_LIMITED

      public static final VideoColor BT601_LIMITED
      BT.601 with studio range. Standard-definition broadcast content.
    • BT601_FULL

      public static final VideoColor BT601_FULL
      BT.601 with full range. The motion-JPEG convention.
    • BT709_LIMITED

      public static final VideoColor BT709_LIMITED
      BT.709 with studio range. High-definition broadcast content.
    • BT709_FULL

      public static final VideoColor BT709_FULL
      BT.709 with full range. High-definition content encoded over the whole code span.
    • BT2020_LIMITED

      public static final VideoColor BT2020_LIMITED
      BT.2020 with studio range. Ultra-high-definition broadcast content.
    • BT2020_FULL

      public static final VideoColor BT2020_FULL
      BT.2020 with full range.
  • Method Details

    • of

      public static VideoColor of(VideoColor.Matrix matrix, VideoColor.Range range)
      Returns:
      the interned instance for matrix and range with a VideoColor.Transfer.SDR transfer; never a new object
      Throws:
      NullPointerException - if either argument is null
    • of

      public static VideoColor of(VideoColor.Matrix matrix, VideoColor.Range range, VideoColor.Transfer transfer)
      Returns:
      the interned instance for matrix, range and transfer; never a new object
      Throws:
      NullPointerException - if any argument is null
    • withTransfer

      public VideoColor withTransfer(VideoColor.Transfer transfer)
      Returns:
      the interned instance with this matrix and range and transfer's curve, or this when the transfer already matches. An unsignalled interpretation given a transfer becomes a signalled one, because a stream that said PQ said something.
      Throws:
      NullPointerException - if transfer is null
    • unspecified

      public static VideoColor unspecified()
      The interpretation for a stream that carried no colour information at all. Decodes exactly as BT709_LIMITED under VideoColor.Transfer.SDR, so a caller that ignores the distinction still gets the common case, but reports false from isSpecified() so a caller that can do better may. A distinct instance, so reference comparison separates "unsignalled" from "signalled as BT.709 limited"; a frame always has a decodable interpretation and this is never null.

      An unsignalled transfer means SDR, and that is a choice rather than an absence. Nearly every file that signals nothing is ordinary display-referred video, and a picture assumed SDR that is really PQ is visibly wrong the instant it is shown; the opposite assumption would make every untagged file wrong instead, all the time. So the fallback is the one that is right almost always and loud when it is not.

      Returns:
      the interned unspecified instance
    • matrix

      public VideoColor.Matrix matrix()
      Returns:
      the encoding matrix; VideoColor.Matrix.BT709 for unspecified()
    • range

      public VideoColor.Range range()
      Returns:
      the code range; VideoColor.Range.LIMITED for unspecified()
    • transfer

      public VideoColor.Transfer transfer()
      Returns:
      the transfer function the decoded values carry; VideoColor.Transfer.SDR unless signalled
    • isDisplayReferred

      public boolean isDisplayReferred()
      Whether the decoded values are already numbers a display can show, which is the question that decides whether a consumer may composite them as an ordinary picture or owes them the inverse of a curve first.
      Returns:
      true exactly when transfer() is VideoColor.Transfer.SDR
    • isSpecified

      public boolean isSpecified()
      Returns:
      whether a stream actually signalled this interpretation, as opposed to it being the fallback a stream that signalled nothing decodes as
    • yOffset

      public int yOffset(int bitDepth)
      Parameters:
      bitDepth - valid bits per component, in [8..16] (PixelFormat.bitDepth())
      Returns:
      the luma code that decodes to 0: 16 << (bitDepth - 8) for studio range, 0 for full range. Studio black is 16 at eight bits and 64 at ten, not 16 at both.
      Throws:
      IllegalArgumentException - if bitDepth is outside [8..16]
    • chromaNeutral

      public int chromaNeutral(int bitDepth)
      Parameters:
      bitDepth - valid bits per component, in [8..16]
      Returns:
      the chroma code carrying no colour difference: 1 << (bitDepth - 1), so 128 at eight bits and 512 at ten, in every range
      Throws:
      IllegalArgumentException - if bitDepth is outside [8..16]
    • yScale

      public double yScale(int bitDepth)
      Parameters:
      bitDepth - valid bits per component, in [8..16]
      Returns:
      luma gain, code to [0..maxCode]: exactly 1 for full range, and for studio range the output span over the input span, 255/219 at eight bits and 1023/876 at ten, which are not the same number
      Throws:
      IllegalArgumentException - if bitDepth is outside [8..16]
    • crToR

      public double crToR(int bitDepth)
      Parameters:
      bitDepth - valid bits per component, in [8..16]
      Returns:
      the Cr contribution to red, per unit of Cr - chromaNeutral(bitDepth)
      Throws:
      IllegalArgumentException - if bitDepth is outside [8..16]
    • cbToG

      public double cbToG(int bitDepth)
      Parameters:
      bitDepth - valid bits per component, in [8..16]
      Returns:
      the Cb contribution to green, per unit of Cb - chromaNeutral(bitDepth); always negative
      Throws:
      IllegalArgumentException - if bitDepth is outside [8..16]
    • crToG

      public double crToG(int bitDepth)
      Parameters:
      bitDepth - valid bits per component, in [8..16]
      Returns:
      the Cr contribution to green, per unit of Cr - chromaNeutral(bitDepth); always negative
      Throws:
      IllegalArgumentException - if bitDepth is outside [8..16]
    • cbToB

      public double cbToB(int bitDepth)
      Parameters:
      bitDepth - valid bits per component, in [8..16]
      Returns:
      the Cb contribution to blue, per unit of Cb - chromaNeutral(bitDepth)
      Throws:
      IllegalArgumentException - if bitDepth is outside [8..16]
    • toString

      public String toString()
      Never throws: VideoColor[BT709 LIMITED], VideoColor[BT2020 LIMITED PQ], or VideoColor[unspecified]. The transfer is printed only when it is not the SDR one, so the common case reads as it always has.
      Overrides:
      toString in class Object