Color emphasis effects on YIQ elements?

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Color emphasis effects on YIQ elements?
by on (#10250)
When using a YIQ-based method to generate a NES palette, like this one by Blargg, what's the most accurate way to do color emphasis? Someone once posted a table of floating point constant factors for each of the 8 color emphasis values, which would be applied to the RGB values to make "emphasized" palettes. However, in the actual hardware, wouldn't the emphasis happen before the YIQ decoding? Does anyone know how to do accurate color emphasis on the YIQ components rather than the resulting RGB?

by on (#10251)
I've implemented this in Nintendulator, though I have no idea if it is actually accurate:

1. Given each palette entry, construct the chroma waveform (in an array of 12 floats or doubles).
2. Given the desired emphasis bits, attenuate the chroma signal.
3. Find the phase offset of your chroma signal. In my emulator, I used a function to find it within 1/5th of a degree in 3 passes (once within 30 degrees, another within 2.5 degrees, and finally within 0.208333 degrees) by comparing the chroma signal to a sine wave (with the same amplitude and offset) at 12 different phases and focusing on the one which resulted in the least absolute error.
4. Find the amplitude of your chroma signal. In my emulator, I simply subtracted the DC offset of the signal (the mean of all 12 values, also used as the luminance) and used its quadratic mean (otherwise known as RMS).
5. Feed your hue, saturation, and luminance into a YIQ->RGB converter [Y = luminance, I = saturation * sin(hue), Q = saturation * cos(hue)] and collect the results.

The results look fairly close to what my composite monitor and TV tuner display when running my Color Bars v2 test program on my CopyNES.

by on (#10252)
The accurate way is to take the discrete Fourier transform of the 12-point signal and discard all but the DC and the first AC coefficient. The DC gives you Y, and the real and imaginary parts of AC give you U and V. YUV is the same thing as YIQ modulo tint. Because you only need three coefficients (real DC, real AC[1], imaginary AC[1]), it'll probably take only 7 multiplies and 29 adds.

by on (#10254)
Some of us don't know how to do a Fourier transform, though, and the Wikipedia article doesn't exactly explain it in a crystal-clear manner...

by on (#10255)
The discrete Fourier transform is essentially dot-producting your signal by a sine wave at each frequency that you want to test. Here's a straightforward way to do it, untested and unoptimized:

Code:
static float sintable[15];

void initSintable(void) {
    int i;
    for(i = 0; i < 15; i++)
        sintable[i] = sin(i * M_PI / 6);
}

float getYUV(const float *signal, float *outY, float *outU, float *outV) {
    double y = 0, u = 0, v = 0;
    int i;

    for(i = 0; i < 12; i++)
        y += signal[i];
    for(i = 0; i < 12; i++)
        u += signal[i] * sintable[i];
    for(i = 0; i < 12; i++)
        v += signal[i] * sintable[i + 3];

    if(outY)
        *outY = y / 12.0;
    if(outU)
        *outU = u / 6.0;
    if(outV)
        *outV = v / 6.0;
}

by on (#10257)
How do you work Hue and Saturation adjustment settings into that?

by on (#10266)
Saturation adjustment ("color" knob) multiplies each U and V by a scale factor. For hue adjustment ("tint" knob), shift the basis functions (sin() and cos() in this case). Given hue in radians, you can do this shift when setting up sintable[]:
Code:
void initSintable(float hue) {
    int i;
    for(i = 0; i < 15; i++)
        sintable[i] = sin(i * M_PI / 6 + hue);
}