Konwersja obrazów do palety Commodore 64

Data publikacji: 2017-08-01 | Tagi:

Ostatnio natchnęło mnie, żeby pobawić się trochę grafiką poczciwego Kommodorka. Ale przy okazji stwierdziłem, że to doskonała okazja, żeby zrobić trochę więcej przy użyciu pythona. Tak oto powstał zabawkowy kod, który łatwo i przyjemnie konwertuje obrazy z palety ponad 16 milionów kolorów, do skromnych 16 kolorów.

Ktoś powie, że takich konwerterów jest na pewno mnóstwo i że nie ma po co tego pisać. Ja odpowiem, że może i są, ale ja żadnego nie napisałem.

Może i nie ma po co, ale jak nie ma po co, to ja i tak napiszę, choćby dla sportu ;).

Krótki background

Commodore 64 w podstawowej, przewidzianej przez projektantów, konfiguracji ma możliwość wyświetlenia jednocześnie 16 kolorów z palety 16 przy rozdzielczości 160x200px. Obłożone jest to dodatkowym warunkiem, że w kwadracie 8x8px mogą być maksymalnie 4 różne kolory.

Za wyświetlanie kolorów odpowiada układ VIC-II i w każdym egzemplarzu komputera kolory mogą się nieco różnić. W przypadku emulatorów każdy z kolorów jest precyzyjnie określony składowymi RGB i odwzorowanie kolorów jest zdecydowanie bardziej powtarzalne. Aczkolwiek są różne podejścia do emulowanych palet kolorów i można znaleź przynajmniej dwie diametralnie różne (zaimplementowane w poniższym kodzie).

Realizacja

Nie skupiałem się na ograniczeniach dotyczących rozdzielczości oraz ograniczeniach dotyczących ilości kolorów w kwadracie 8x8px. Skupiłem się jedynie na zmianie palety.

Przy liczeniu najbliższego pasującego koloru zastosowałem najprostszy możliwy algorytm Euklidesa.

Zastosowałem dwie przykładowe palety do wyboru: stąd oraz stąd.

Kod

import math

from PIL import Image

COLOR_BLACK = (0, 0, 0)
COLOR_WHITE = (255, 255, 255)
COLOR_RED = (136, 0, 0)
COLOR_CYAN = (170, 255, 238)
COLOR_VIOLET = (204, 68, 204)
COLOR_GREEN = (0, 204, 85)
COLOR_BLUE = (0, 0, 170)
COLOR_YELLOW = (238, 238, 119)
COLOR_ORANGE = (221, 136, 85)
COLOR_BROWN = (102, 68, 0)
COLOR_LIGHTRED = (255, 119, 119)
COLOR_GREY1 = (51, 51, 51)
COLOR_GREY2 = (119, 119, 119)
COLOR_LIGHTGREEN = (170, 255, 102)
COLOR_LIGHTBLUE = (0, 136, 255)
COLOR_GREY3 = (187, 187, 187)

C64_PALETTE = (
    COLOR_BLACK,
    COLOR_WHITE,
    COLOR_RED,
    COLOR_CYAN,
    COLOR_VIOLET,
    COLOR_GREEN,
    COLOR_BLUE,
    COLOR_YELLOW,
    COLOR_ORANGE,
    COLOR_BROWN,
    COLOR_LIGHTRED,
    COLOR_GREY1,
    COLOR_GREY2,
    COLOR_LIGHTGREEN,
    COLOR_LIGHTBLUE,
    COLOR_GREY3,
)


COLOR2_BLACK = (0, 0, 0)
COLOR2_WHITE = (255, 255, 255)
COLOR2_RED = (103, 55, 43)
COLOR2_CYAN = (112, 164, 178)
COLOR2_VIOLET = (111, 61, 134)
COLOR2_GREEN = (88, 141, 67)
COLOR2_BLUE = (53, 40, 121)
COLOR2_YELLOW = (184, 199, 111)
COLOR2_ORANGE = (111, 79, 37)
COLOR2_BROWN = (67, 57, 0)
COLOR2_LIGHTRED = (154, 103, 89)
COLOR2_GREY1 = (68, 68, 68)
COLOR2_GREY2 = (108, 108, 108)
COLOR2_LIGHTGREEN = (154, 210, 132)
COLOR2_LIGHTBLUE = (108, 94, 181)
COLOR2_GREY3 = (149, 149, 149)

C64_PALETTE2 = (
    COLOR2_BLACK,
    COLOR2_WHITE,
    COLOR2_RED,
    COLOR2_CYAN,
    COLOR2_VIOLET,
    COLOR2_GREEN,
    COLOR2_BLUE,
    COLOR2_YELLOW,
    COLOR2_ORANGE,
    COLOR2_BROWN,
    COLOR2_LIGHTRED,
    COLOR2_GREY1,
    COLOR2_GREY2,
    COLOR2_LIGHTGREEN,
    COLOR2_LIGHTBLUE,
    COLOR2_GREY3,
)


class Converter(object):

    def __init__(self, input_image_name, output_image_name, palette):
        self.input_image_name = input_image_name
        self.output_image_name = output_image_name
        self.palette = palette

    def _calc_distance(self, pixel, color):
        return math.sqrt((color[0] - pixel[0]) ** 2 + (color[1] - pixel[1]) ** 2 + (color[2] - pixel[2]) ** 2)

    def _get_closest_color(self, pixel):
        distances = []
        for color in self.palette:
            distances.append((color, self._calc_distance(pixel, color)))

        sorted_distances = sorted(distances, key=lambda item: item[1])

        return sorted_distances[0][0]

    def convert(self):
        input_image = Image.open(self.input_image_name)
        in_image = input_image.load()

        width, height = input_image.size

        output_image = Image.new('RGB', (width, height))
        out_image = output_image.load()

        for x in range(width):
            for y in range(height):
                out_image[x, y] = self._get_closest_color(in_image[x, y])

        output_image.save(self.output_image_name)


c = Converter('test.png', 'test_a.png', C64_PALETTE)
c.convert()

c = Converter('test.png', 'test_b.png', C64_PALETTE2)
c.convert()

Nie dodałem obsługi argumentów z linii poleceń itp. Bo zwyczajnie nie uważam tego za jakąś poważną aplikację, a jedynie projekt for fun.

Aby uruchomić konwerter musimy mieć testowy png (o nazwie test.png) w katalogu ze skryptem, a następnie uruchomić skrypt poleceniem: python nazwa_skryptu.py. W efekcie w tym samym katalogu pojawią się dwa nowe obrazy png przekonwertowane do palety pierwszej (test_a.png) oraz drugiej (test_b.png).

Przykładowe konwersje

Oryginał:

Paleta A:

Paleta B:

Oryginał:

Paleta A:

Paleta B:

Oryginał:

Paleta A:

Paleta B:

Podsumowanie

Wprawne oko od razu wyczuje obecność kolorów z Commodore 64 ;). Natomiast co do celności palet uważam, że jedna i druga ma w sobie coś ciekawego. Pierwsza daje bardziej "kolorowe" odwzorowania, druga bardziej stonowane i delikatniejsze. Obie są wspaniałe! :)


Oceń ten post:
Podziel się:

comments powered by Disqus

IT w obrazkach: