A new NTSC palette generator

Discuss emulation of the Nintendo Entertainment System and Famicom.

Moderator: Moderators

AWJ

A new NTSC palette generator

Post by AWJ » Mon Nov 29, 2004 1:48 am

Written in Python. Uses a different transformation to get the RGB values than Kevin's. I've tried the palette it generates in FCEU and it looks much better than Kevin's does with some games (like FF3j) The docstrings and the sites referenced in them pretty much explain everything.

Code: Select all

"""AWJ's NTSC NES palette generator.

Based on Kevin Horton's palette generator
and on information from these web pages:
http://unusedino.de/ec64/technical/misc/vic656x/colors/
http://en.wikipedia.org/wiki/YIQ
http://en.wikipedia.org/wiki/YUV
"""

import array
import math

hue = (0, 285, 315, 345, 15, 45, 75, 105, 135, 165, 195, 225, 255, 0, 0, 0)
"""Table of hue values (theta coordinate on Q-I plane)

Assumption:  That the 12 non-grey hues are equally spaced
30 degrees apart, and offset 15 degrees from the axes.

This assumption allows hue 8 (colors 08, 18, 28, 38) to be
very close to pure yellow (R = G > B), and for hue 6 (06, etc.)
to be fairly close to pure red (R > G = B).

The alternate assumption, that every third hue is aligned with
an axis, results in no hue being close to pure yellow or pure red.

TODO: (by someone else)
verify hues using a video signal separator and a real NES
"""

sat = 0.146
"""Saturation value (radius coordinate on Q-I plane)

Assumption:  That all 12 non-grey hues have the same saturation.
According to Kevin Horton (and parsimony) this is the case.

Currently this is an arbitrary value, chosen such that
color 08 ends up with a blue level of exactly zero.
"""

luma = ((0.50, 0.75, 1.00, 1.00),
        (0.29, 0.45, 0.73, 0.90),
        (0.00, 0.24, 0.47, 0.77))
"""Table of luma (Y) values

Assumption:  That these values (stolen from Kevin Horton's
QBASIC palette generator) are accurate :)

Are color 00 and 2D, and 10 and 3D, really this close?
All hand-hacked NES palettes I've seen make them
distinctly different shades of grey from each other
(3D brighter than 10, and 2D much darker than 00)

TODO: (by someone else)
verify values, ideally on several hardware versions
(frontloader, toploader, original Famicom, AV Famicom)
"""

def yiq2rgb(y, i, q):
    """Convert a YIQ color to RGB.

    Transformation matrix stolen from here:
    http://www.mathworks.com/access/helpdesk/help/toolbox/images/ntsc2rgb.html
    """

    r = y + 0.956 * i + 0.621 * q
    g = y - 0.272 * i - 0.647 * q
    b = y - 1.106 * i + 1.703 * q
    return (r, g, b)

# Main program begins here

nespalette = array.array('B')

for i in range(4):
    for j in range(16):
        h = math.radians(hue[j])
        if j > 13:
            s = 0
            y = 0
        elif j == 0:
            s = 0
            y = luma[0][i]
        elif j == 13:
            s = 0
            y = luma[2][i]
        else:
            s = sat
            y = luma[1][i]

        r, g, b = yiq2rgb(y, s * math.sin(h), s * math.cos(h))

        # clip RGB values
        if r < 0.0: r = 0.0
        if g < 0.0: g = 0.0
        if b < 0.0: b = 0.0
        if r > 1.0: r = 1.0
        if g > 1.0: g = 1.0
        if b > 1.0: b = 1.0

        # TODO: add gamma correction

        # convert from [0..1] to [0..255]
        r = round(r * 255.0)
        g = round(g * 255.0)
        b = round(b * 255.0)

        # add this color to the palette
        nespalette.fromlist([int(r), int(g), int(b)])

outputfile = file("awj.pal", "wb")
nespalette.tofile(outputfile)
outputfile.close()

AWJ

Post by AWJ » Tue Nov 30, 2004 7:08 am

Hmm... I see plenty of views but no replies. Let me try to explain what's going on a bit more, particularly what the deal is with this YIQ stuff.

As you'll know if you've hooked up a DVD player, a television video signal is composed of three components: a brightness or "Luma" (Y) component plus two components that determine color, a blue component (called U, Pb or Cb depending on scaling) and a red component (V, Pr or Cr).

Colorspaces based on brightness, blue and red components are called YCC colorspaces and there are many different ones, the difference between them being the scaling of the three components with respect to each other. The colorspace color television uses, whether NTSC or PAL, is YUV. The colorspace digital video on a DVD uses, on the other hand, is YCbCr. This gets converted to YUV by multiplying the blue component by one constant and the red component by another.

The reason Kevin Horton's old palette generator produces inaccurate colors (oversaturated reds and weak blues) is that he mistakenly used an RGB conversion formula for one of the other YCC colorspaces out there, not YUV.

Inside the innards of your television set, the YUV components undergo a linear transformation to get RGB values. The Wikipedia entry for YUV includes the matrix to transform from RGB to YUV. What a television performs, obviously, is the mathematical inverse of that transformation.

Now, the color information in a color NTSC signal (the stuff that comes in on the "C" pin on an S-Video jack, or the "Chroma" RCA jack on an oldschool Commodore monitor) consists of two components: I and Q. These are not the red and blue color components, like you might expect; rather, they're a pair of coordinates on a plane which need to be rotated around the origin in order to get V and U. The hue (aka tint) control on a television controls the angle of rotation.

The reason NTSC uses this seemingly Rube Goldberg-esque setup, which is responsible for the unflattering alternate expansion "Never The Same Color", has to do with human color perception and optimal usage of bandwidth. There's a brief explanation in the Wikipedia entry for YIQ. Basically the I axis, which distinguishes the hues to which humans are most sensitive, gets the lion's share of bandwidth--in a standard broadcast signal, I uses nearly three times as much bandwidth as the less important Q.

Note that a PAL color signal is completely different from NTSC, and actually directly encodes U and V, the blue and red components. No rotation is required, and therefore PAL television sets don't need a hue control (and don't have one, unless they're dual-standard TVs that can take NTSC input)

The "canonical" angle to rotate the I and Q coordinates by in order to get V and U is 33 degrees. In other words, a television with hue correctly calibrated (by using a standard color bar pattern) will rotate the coordinates by exactly this angle. The matrix found in the yiq2rgb() function in my program combines in a single transformation the 33° rotation of I and Q, with the conversion from YUV to RGB.

So what has all of this got to do with the NES? Well, the NTSC NES PPU's video output is a composite NTSC signal consisting of Y, I and Q components. If we know what values of Y, I and Q the PPU produces for a given palette color, we can use the YIQ-to-RGB transformation to get the exact RGB colors that will be generated on a canonically calibrated television.

Several years ago, Kevin Horton determined the Y value for each NES color and incorporated his findings into his palette generator (though as I mention in the program documentation I'm just a little bit suspicious of some of those values, and it would be nice if someone could reproduce and confirm them). Determining experimentally the I and Q values for each hue is not as easy to do; you can buy a Y/C splitter for ten bucks, but splitting the C signal into I and Q requires more specialized hardware. Therefore, for now, we have to settle for determining them inductively.

The most parsimonious theory is that the I and Q values for the twelve nongrey hues are evenly spaced points on a circle. Furthermore, it is most likely that the twelve points are either aligned on the I and Q axes or that the axes pass midway between two adjacent points. The reason is that if either of these is the case, then the PPU only needs to be able to generate 6 different nonzero levels of I and 6 different nonzero levels of Q. If the twelve points are not aligned with the axes, on the other hand, the PPU has to be able to generate 12 different nonzero levels of I and Q.

Given that hue 8 is known to be more or less yellow, there are three arrangements of the 12 hues that seem reasonable:

1: Hue 6 lies on the positive I axis. This makes hue 8 slightly green.
2: Hue 7 lies on the positive I axis. This makes hue 8 slightly orange.
3: The positive I axis passes midway between hues 6 and 7. This makes hue 8 almost pure yellow.

My program goes with arrangement 3, for two reasons. The first is simply that it's the median hypothesis out of the three candidates. The second is that I tested all three with an assortment of commercial games, including games whose colors on the real console I remember well (my NES is in a closet a few hundred miles away, but the colors of Ultima: Quest of the Avatar's overworld are permanently etched into my brain) The games were evenly split between those that looked good with 1 and 3, and those which looked good with 2 and 3. The clincher game was Final Fantasy 3 (despite the fact that I've never played it on a real NES) Both 1 and 2 had obvious problems with this game: 1 resulted in very purple water, while 2 resulted in brownish grass and trees in towns.

The other half of the question is the saturation--the radius of the circle plotted by the 12 hues. If the saturation is too high, the RGB values for some colors go out of gamut and have to be clipped, resulting in distorted hues. What I did was choose the largest saturation such that no color in the lower two brightnesses suffered clipping. This was probably too conservative of me--the hues are certainly accurate, but the colors are quite dull. What I'm going to try next is to use the largest saturation such that no color's red component goes out-of-gamut (Blue is the first component to go out-of-gamut with excessive saturation, followed by red and finally green)

User avatar
baisoku
Posts: 121
Joined: Thu Nov 11, 2004 5:30 am
Location: San Francisco, CA
Contact:

Post by baisoku » Fri Dec 03, 2004 5:48 am

i have implemented the ntsc -> rgb conversion using the yiq transformation matrix, and i am quite happy with the results.

i am using a simplified version of kevin's converter algorithm, with luma values at regular intervals: 0.0, 0.25, 0.5, 0.75, 1.0.

there's also no need for the color angle table. color angles simply start at 255 degrees (or whatever your "tint" preference is) and increase 30 degrees for each hue.

good work on this.
...patience...

User avatar
laughy
Posts: 41
Joined: Wed Nov 17, 2004 12:34 pm
Contact:

:)

Post by laughy » Fri Dec 03, 2004 10:33 am

This is good stuff here.

Here's the rgb output if anyone wants it (as I would) (in order):

[r, g, b]

(row 1)
[128, 128, 128]
[46, 77, 130]
[65, 64, 148]
[87, 53, 146]
[105, 48, 125]
[115, 50, 90]
[114, 58, 51]
[102, 70, 18]
[83, 84, 0]
[61, 95, 2]
[42, 100, 23]
[32, 98, 58]
[34, 90, 97]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]

(row 2)
[191, 191, 191]
[86, 118, 171]
[106, 105, 189]
[128, 94, 187]
[146, 89, 165]
[156, 91, 130]
[155, 99, 91]
[143, 111, 59]
[124, 125, 41]
[102, 135, 43]
[83, 141, 64]
[73, 139, 99]
[74, 131, 138]
[61, 61, 61]
[0, 0, 0]
[0, 0, 0]

(row 3)
[255, 255, 255]
[158, 190, 242]
[177, 176, 255]
[199, 166, 255]
[218, 160, 237]
[228, 162, 202]
[227, 170, 163]
[215, 183, 130]
[195, 196, 112]
[173, 207, 114]
[155, 212, 136]
[145, 210, 170]
[146, 202, 210]
[120, 120, 120]
[0, 0, 0]
[0, 0, 0]

(row 4)
[255, 255, 255]
[201, 233, 255]
[221, 220, 255]
[243, 209, 255]
[255, 204, 255]
[255, 205, 245]
[255, 213, 206]
[255, 226, 173]
[238, 239, 156]
[216, 250, 158]
[198, 255, 179]
[188, 254, 214]
[189, 246, 253]
[196, 196, 196]
[0, 0, 0]
[0, 0, 0]

Nessie
Posts: 134
Joined: Mon Sep 20, 2004 11:13 am
Location: Sweden
Contact:

Post by Nessie » Fri Dec 03, 2004 3:16 pm

And here it is in binary format:
NTSC.pal

User avatar
laughy
Posts: 41
Joined: Wed Nov 17, 2004 12:34 pm
Contact:

;)

Post by laughy » Fri Dec 03, 2004 7:54 pm

Image

Great Hierophant
Posts: 745
Joined: Tue Nov 23, 2004 9:35 pm

Post by Great Hierophant » Fri Dec 03, 2004 8:33 pm

Do you realize there is not a primary color anywhere near that palette? That is awfully faded or pastel-like, surpassing FCE Ultra's NTSC emulation.

AWJ

Post by AWJ » Fri Dec 03, 2004 11:47 pm

Great Hierophant wrote:That is awfully faded or pastel-like, surpassing FCE Ultra's NTSC emulation.
That's due to the default saturation in the first version being too low, as I said. I've subsequently changed it to 0.221. Saturation is very much an "adjust-to-taste" setting: the only way to get a canonical value would be to compare the PPU's chroma output to the standard NTSC color bar pattern. If you incorporate this formula into a NES emulator then saturation (and gamma, which I've also subsequently added) should be user-adjustable.

To incorporate gamma correction, add the following lines to the program after clipping the RGB values:

Code: Select all

# correct gamma
r = pow(r, 2.2 / gamma)
g = pow(g, 2.2 / gamma)
b = pow(b, 2.2 / gamma)
You'll also have to add a value for gamma to the program, of course. The default value should probably be 2.2, which results in no gamma correction.

The next frontier in palette emulation is to determine what the color emphasis bits do at the level of NTSC output. I have a theory, but no way to confirm it or to get the exact numbers, since I don't have a devcart.

NewRisingSun
Posts: 1042
Joined: Thu May 19, 2005 11:30 am

Post by NewRisingSun » Thu May 19, 2005 12:05 pm

Nice. A couple of points:
we can use the YIQ-to-RGB transformation to get the exact RGB colors that will be generated on a canonically calibrated television.
Note that most NTSC consumer TV sets are not and can't ever be "canonically calibrated", older devices even less than newer and cheaper devices less than expensive ones. Using the by-the-book decoder matrix will result in a palette that is "accurate" in that it looks the same as on a studio-quality monitor, but not "accurate" in that it looks the way the games were designed to look.
NES graphics artists most likely used and optimized for consumer-grade equipment, which systematically and intentionally deviates from the NTSC specification (i.e. wide-angle color demodulation, flesh-tone boost); with Japanese TV sets using different algorithms than US TV sets (for example, Sony TVs have an "AXIS" setting in service mode that can be set to "US" or "JAPAN").
Hue 6 lies on the positive I axis. This makes hue 8 slightly green.
This one is correct. Hue 8 is a yellowish green on a NTSC-faithful color decoder at the "canonical" hue setting.
Both 1 and 2 had obvious problems with this game: 1 resulted in very purple water, while 2 resulted in brownish grass and trees in towns.
That's exactly the problem you're always going to have with a 100% faithful NTSC decoder, no matter what your tint setting is. All Japanese games are designed for Japanese consumer TV sets, which place the I and Q axis about 105 to 120 degrees apart (standard NTSC uses 90 degrees, as we know) and boost yellows (I>Q colors) to make Asian fleshtones more "natural" on the "colder" 9300K phosphors that they use in Japan.

Drag
Posts: 1285
Joined: Mon Sep 27, 2004 2:57 pm
Contact:

Post by Drag » Thu May 19, 2005 8:17 pm

I made a hack to the palette generator that generates one I like a little better.

It doesn't matter how saturated I make the colors, blue always ended up too strong, and red always ended up too weak, so I just flip flopped them (red got outputted to b, and blue got outputted to r), and then I adjusted the hues to make the colors right, and it made a nice palette... the yellow got slightly more brown as it got darker, and red now can get strong without blue getting over saturated.

But that was just my personal preference, and I know it's probably not accurate, but it looks good to me. I read that red on a TV was the strongest color... like if you turned the color saturation WAY up, the red would get really strong and bleed into the other colors first.

User avatar
blargg
Posts: 3715
Joined: Mon Sep 27, 2004 8:33 am
Location: Central Texas, USA
Contact:

Post by blargg » Fri May 20, 2005 2:37 am

Isn't it impossible to emulate the NES color accurately without handling where pixels fall relative to the pixel clock or something like that? This seems the direction to go for accurate color rendering. A palette can only go so far, and if the emulator isn't taking the effect of the pixel's position into account, each palette is a tradeoff and will make some games look better than others.

NewRisingSun
Posts: 1042
Joined: Thu May 19, 2005 11:30 am

Post by NewRisingSun » Fri May 20, 2005 4:21 am

That should only apply to "artifact colors", like on the Apple ][ or the CGA's 160x200 composite color mode, where the color is determined by which monochrome pixel columns are illuminated.

AWJ

Post by AWJ » Mon Jun 27, 2005 6:57 am

NewRisingSun wrote:Nice. A couple of points:

(snip)

NES graphics artists most likely used and optimized for consumer-grade equipment, which systematically and intentionally deviates from the NTSC specification (i.e. wide-angle color demodulation, flesh-tone boost); with Japanese TV sets using different algorithms than US TV sets (for example, Sony TVs have an "AXIS" setting in service mode that can be set to "US" or "JAPAN").
This is very interesting information. Can you provide more details as to how US and Japanese sets differ from each other and from the NTSC specification, or does it vary from manufacturer to manufacturer?

tepples
Posts: 21746
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Post by tepples » Mon Jun 27, 2005 7:05 am

If you do not emulate "artifact colors", then the bricks in underground levels of Super Mario Bros. won't look like those on real NES hardware. Instead, they might look like the bricks on a VS Unisystem or a PlayChoice 10.

Anyway, the output of the PPU in palette values $x1-$xC is a square wave that toggles between the values used for the $x0 and $xD gray series. This should help you determine the correct saturation.

As for hue spacing, at least $x1, $x3, $x5, $x7, $x9, $xB are spaced evenly, and so are $x2 through $xC even. The spacing between $x1 and $x2, between $x3 and $x4, and so on seems like it would depend on the duty cycle of the master clock (21.48 MHz, 6*colorburst), as the PPU's hue generator is based on a 12-step counter clocked on positive and negative edges.

Guest

Post by Guest » Mon Jun 27, 2005 7:19 am

tepples wrote:Anyway, the output of the PPU in palette values $x1-$xC is a square wave that toggles between the values used for the $x0 and $xD gray series. This should help you determine the correct saturation.
So colors 01-0C oscillate between the values of 00 and 0D, 11-1C oscillate between 10 and 1D, et cetera? That would mean that the lightest row of non-gray colors is approximately half as saturated as the other three rows. Great information. How accurate are kevtris's values for the luminances of the gray colors?

I daren't suppose you know the details of how the color emphasis bits affect the output...

"artifact colors" are unfortunately outside the scope of a simple palette-generating algorithm, though it would be interesting (but probably beyond my skill) to add them to an emulator.
NewRisingSun wrote:
Hue 6 lies on the positive I axis. This makes hue 8 slightly green.
This one is correct. Hue 8 is a yellowish green on a NTSC-faithful color decoder at the "canonical" hue setting.
Aha, it must be about the same hue as the uniforms in the original Star Trek series. The actual uniforms were pale green, but they appeared yellow on consumer television sets.

Post Reply