2015/09/07

Analyzing Color Palettes: Introducing Impale.fs

As announced last week, I've been working on an image analysis utility called Impale. It's just 39 lines of F#, but not being used to the language and fucking around with various plotting frameworks (none of which I got to work in a reasonable time) led it to take like 12 hours of work.

You can find the code here, but we'll be going over it now anyway, before we apply it and see if we can learn something new about pixel art and color palettes.

The Source Code

First of all, we open some stuff, which is essentially using in C# terms. We use System.Drawing to read image files, System.IO to write CSV files, and I had to add System.Globalization because my PC is running a German locale, which means I usually can't just use ToString on floating point numbers and expect it to play nicely with basically any tool ever made in the US. Which is practically any tool, ever.

We then define the main function and mark it as the EntryPoint, which could be any top-level function in an F# program, I just decided to name it main anyway. It doesn't actually do anything interesting by itself; we just define a local function analyze, and use Array.Map to apply it to all arguments that were passed to the program. Finally, we return 0, for no particular reason, other than not bothering to look up how to properly return unit, the F# equivalent of void.
Note that map actually returns a new array, where each element corresponds to the result of analyze when applied to an element in args. Since we only call analyze for its side effects (creating two files), we have to ignore the result explicitly, since F# expects us to write programs without side effects.
ignore is actually a function itself: the |> operator redirects the value on its left into the function on its right.

The analyze function

This is where all the actual work happens. We start off by opening a bitmap, and converting it into a flat list of colors using list comprehension. We then use List.distinct (which doesn't seem to appear on MSDN for some reason) to get the set of all colors in the bitmap, except it's still a list, not an actual set.

We then define writeLine, a local function that prints a properly formatted (float, float) tuple to a StreamWriter.
We also define getValueSaturation, which extracts such a tuple from a Color instance using the GetBrightness and GetSaturation methods. We then get our first set of data points using List.map again.
Next, we create a StreamWriter to a (hopefully) new CSV file and use partial function application to create an anonymous function based on the writer and the writeLine function. If you think of it as a family of functions distinguished by the StreamWriter they use, this is a pretty cool form of metaprogramming.
Finally, we close the stream because we're good citizens.

Room for improvement

These last couple steps are then repeated with a different file extension and a different color-to-data-point conversion function. Since you can pass functions around easily, this is actually redundant code, I just copy-pasted it as an afterthought after I got the first step working.

We could also have done a more sophisticated data analysis, for example by grouping the colors into color ramps, adding more valuable information.

Since I didn't want to bother getting XPlot to work, the script currently puts out CSV files which I have to manually paste into fooplot. Visualizing data automatically would be a great boon.
At least I don't have to manually gather color information with Paint.NET or something...


But... why?

Essentially, you can learn pixel art by trial and error. You can get better quicker by looking at other people's work. Outlining, dithering, applying texture to a surface. I feel like I'm reasonably good at those techniques, and you can learn them by zooming in and studying images that do them well.

However, learning to properly construct palettes is hard. You need to reconstruct the palette from the image to get an overview, you need to understand basic color theory and understand how the palette relates to the image as a whole.
Just extracting the palette is tedious work and easy to mess up. Visualizing it in a meaningful way to understand why it works is itself pretty boring, but necessary to perform any kind of analysis.
This tool is supposed to help with these two steps in particular.

We gather all colors in the image and spit out two scatter plots in CSV: One shows the relationship between saturation and brightness, the other between hue and brightness.
Let's look at a couple of examples to see how that can be an immense help in palette analysis.

Pokémon Garnet

Okay, so this is a long dead RPG Maker XP based fan game, but it's easily the best looking fan game I've ever seen, it arguably looks better than the original Pokémon games, and it's up there on my list of best looking pixel art game, full stop.

I mean, just look at this:


This game is gorgeous! And that's because it has an obviously carefully crafted color palette.
Of course, the line work and shading are pretty good, too, but most of these tiles are recolors from freely available tile sets; they just aren't the distinguishing factor.
The palette alone isn't enough to make it stand out, though. The levels (or, at least, the few screenshots we have, the game never saw a release) are actually designed with the palette in mind: Each area has a very distinct color scheme.
Anyway, let's do this analysis.

First of all, we need to beware of the screen overlays this game uses. We could keep them in, but since I plan on using some too, we should only run the analysis on portions of the screen that aren't affected by the overlay.
For instance, the scene at the beach is complicated by god rays and lens flare. Using the magic wand tool with global selection and zero percent tolerance, I selected and copied an area of the screen that wasn't overlaid with anything and ran the analysis on that.
Starting with x=Brightness; y=Hue for the beach scene
Immediately, we can see clear bands of color. That's the color scheme I mentioned. We have some obvious outliers; the medium-bright purple/pink at the top is the NPC in the top right corner, the really bright blues on the right are at the water/land border, where there's spume. The yellow near the middle of the lower left quarter is probably from one of the NPCs as well, though it's not obvious.
Other than that, there's two obvious groups, one in the 0-45 degree range, the other in the 180-225 degree range. So we're actually dealing with near-complementary colors.
Furthermore, both groups can be split into at least two color ramps each. It's a little difficult to tell from the diagram alone, but we know that the orange group is made up of the light, yellow sand and the dark, brown rock, while the water is split into the light blue rock surface and the dark blue rock walls. While it's not necessarily easy to tell which color belongs to which ramp, it's clear there's no definite trend within a ramp. For example, I remember quite a few tutorials saying that as colors get brighter, their hue should trend towards 60 degrees, i.e. yellow, making the light source appear yellow. Here, there seems to be mostly steady hue within a ramp.
x=Brightness; y=Saturation, also for the beach scene
For saturation in relation to brightness, we see a general upwards trend, with few colors in the lower right and upper left quarters. We can also see that the density in the lower left is a lot higher. There are few obvious color ramps, though.
You might also notice that, other than black for sprite outlines, in this image there's exactly zero colors that have less than ten percent value or saturation.

I'm not going to write a full analysis like this for every screenshot I have, but looking at some other saturation/brightness diagrams, there's one rule that feels really important: saturation and brightness rarely sum to more than one, and when they do, it's usually on an interactive object.
You can see this for yourself by drawing the function f(x)=1-x. Only few colors will ever fall above the line. For example, this is from the top-right forest scene:
Of course, this diagram doesn't show the general upwards trend, and there's colors with less than ten percent brightness and less than ten percent saturation, though there's still no zeros there, and none with both values in the deep end.
In fact, in this scene, if you were to color code the dots by which ramp they belong to (which I actually did using Paint.NET and Excel one time, motivating me to write the script), you'd notice that the saturation in each color ramp decreases as brightness increases.
In the portion of the screenshot I analyzed, there's no NPCs visible. The only colors above the previously mentioned line are outlines of collidable background objects. If I added NPCs, the top right of the chart would get filled in more.

When combined with the black outlines Sprites use in this game, this is a great way to lead the eye and to make interactivity visually obvious without placing silly exclamation points above characters' heads.

Conclusion

All in all, I think this was totally worth the two evening-nights I actually worked on the script and the past couple hours I spent analyzing this. As always, we could go more in depth, both with the script and our manual interpretation, but I think that's enough for today.

See you!

No comments:

Post a Comment