„Zkouším sdělit něco z hloubek, kam se člověk obvykle nedívá. Možná to nebude pro každého – ale kdo se rozhodne jít se mnou až do konce, toho chci vzít tam, kde jsem sám hledal.“
První kroky do lesa
Každý list má svůj stín. A každý obraz moment, kdy se rozhodne, jestli bude žít, nebo zůstane jen grafikou. Tohle je o těch chvílích mezi.
Ukážu ti, jak jsem tvořil paralaxu pro začátek hry – ještě než se zvedne opona a objeví se to první malé demo, ve kterém si poprvé zkusíš pohnout postavou a projít se tímhle světem.
Na první pohled si možná řekneš, že tvorba grafiky pro Amigu je jen o skládání barevných čtverečků do nějakého tvaru. Ale jakmile se do toho ponoříš hlouběji, zjistíš, že pixel art není jen technika – je to způsob, jak vdechnout život statickému obrazu. V tomhle článku tě vezmu na cestu lesem, který se teprve začíná skládat – ukážu ti metody úprav, jejich výhody i slepé uličky. A možná se spolu dostaneme do chvíle, kdy i zdánlivě malá změna dokáže obrátit celý pocit ze scény.
První obraz: kde se to celé zlomilo
Les, který jsem hledal, nebyl v počítači. Byl ve mně – dávno předtím, než jsem začal klikat pixely. Objevoval se ve snech, v ranních procházkách mlhou, v dětství, když jsem se prodíral houštím a v dálce zahlédl světlo na mýtině. Nešlo o vzhled. Šlo o ten pocit, že jsi někde, kde svět mluví šeptem.
A když jsem pak narazil na obrazy Eyvinda Earla, měl jsem pocit, že někdo jiný ten les našel dřív než já.
A právě kvůli tomu jsem narazil na Eyvinda Earla. V jeho obrazech bylo něco co jsme viděli oba. Když se člověk koukne na strom a zpravidla jen přejde, tak si na první pohled nevšimne jaký obrys a hlavně rytmus tvoří všechny větve. Nelehne si pod strom a nepozoruje listy proti slunci, kdy se přepálí barvy jeho okrajů a prosvítají žilky a brouci na povrchu. Nevidí svět mnoho let z jednoho místa.
Právě díky nim jsem pochopil, že světlo není jen barva navíc – je to nálada. V Earlových obrazech světlo často padá na kraj koruny, na jediný pruh trávy, někdy jen na prázdno mezi dvěma stromy. A přesto to stačí. To světlo ví, kde má být. A já jsem si uvědomil, že přesně takové momenty chci do své hry. Ne velké efekty. Ale les, který se ti zadře pod kůži. Les, co dýchá pomalu. A nikdy nahlas.
Fáze obrazu: Od vidiny k formě
Ten první obraz je přímo od Eyvinda (Fired by sun). Pojmenoval jsem ho jednoduše: Earl01. Byl to začátek. Obraz, který ve mně dlouho existoval jako pocit, ale nikdy ne jako forma. Věděl jsem, co chci cítit, ale ne, jak to vypadá. A tak jsem hledal.
Pomohla mi i AI. Ne jako řešení, ale jako zrcadlo. Věci, které jsem nosil v sobě, se najednou objevily v náznaku tvaru, rytmu, světla. Neuměl bych je v tu chvíli nakreslit od nuly — a možná bych tím zabil přesně tu křehkou náladu, kterou jsem hledal. Ale ten obraz tam najednou byl. Ne hotový. Ale otevřený.
Earl01 – vstupní brána. Nálada, která zatím neví, co přesně chce být.
Obrázek je reprodukcí díla Eyvinda Earla. Oskenováno z mého výtisku knihy „The Complete Graphics of Eyvind Earle and Selected Poems & Writings 1991–2000“, část Fired by Sun. Slouží jako inspirační referenční bod pro vlastní pixelartové zpracování.
Pak následovala Phase02. A pak Phase03. A další desítky pokusů, slepých uliček a škrtanců. V každé fázi jsem něco přetvářel, něco smazal, něco přidal. A v tom procesu se začal rodit můj les. Ne jako výsledek klikání — ale jako místo, kde jsem najednou poznával barvy, které mi dávno patřily.
Záznam úvahy, která vedla ke vzniku Phase03:
„Fór je v tom, že trpaslík bude muset mít alespoň 12 barev. A i jeho domek a všechny předměty ve hře budou muset mít také barvy. Nemohou zůstat ve stínu. Budu muset udělat trik s hlavní vrstvou pozadí (přemýšlel jsem o 2bitovém pozadí – tedy 4 barvy), kde by díky ditheringu byly modré a zelené odstíny jako to, co tam vidíš na pozadí. Zkus vyextrahovat pozadí z toho obrázku a dopočítat ho na celou šířku. Jak by to vypadalo?“
Tahle zpráva původně zazněla jen jako rychlý ping na AI. Ale právě ona otevřela nové dveře.
Výsledek mě překvapil. Najednou jsem měl kus obrazu, který nevznikl kreslením – ale zúžením úvahy na jedinou otázku: co musí být zůstat vidět.
AI se podle mé věty, soustředila na modré a zelené odstíny, vyextrahoval je z předchozího obrázku a dopočítala. Je možné, že si také zapamatovala zmíňku o siluetách, kterou jsem předítm napsal a zohlednila ji.
Phase03 – návrat k paměti. Hledání tvaru, který si myslím, že si pamatuju, ale ještě není zřetelný. Mlžný obraz lesa, kde se něco rýsuje, ale nedá se chytit.
Phase04 – rozjasnění. To, co bylo předtím mlhavé, se náhle vyjasnilo. Kontury, rytmus, světlo – téměř jistota. Tady už les ví, že je les. A já vím, že jsem v něm byl. A opět jsem.
Bitplány jako rozhodnutí, ne omezení
V určité chvíli ve mně vyvstala jiná otázka. Ne „co Amiga umí“, ale kolik toho zvládne, když se přestanu bát jít na to trochu chytře – ale i trochu hrubě.
Začal jsem přemýšlet, jak strukturovat obraz tak, aby vypadal, jako by měl o třídu víc výkonu – bez přepínání režimů, bez kouzel, bez magie. Jen s rozumným rozložením vrstev.
Nešlo o to natlačit do obrazu co nejvíc detailu. Chtěl jsem kontrast – temné větve vpředu, hluboké světlo vzadu. Postava ve světle. A celou scénu tak, aby mohla dýchat. Dlouho jsem uvažoval spolu s AI o klasickém přístupu: dvě vrstvy po jednom bitplánu, plus šest bitplánů pro hlavní obraz. Ale něco tomu chybělo.
A pak to přišlo.
Vrstvy jsem rozvrhl nejen barevně, ale i podle způsobu zobrazení – bitmapa, tile scrolling siluet, samostatné objekty. Nešlo jen o výkon. Šlo o to, aby každá složka scény měla jasně vymezené místo – hloubkově i barevně.
- Pozadí (scroll X1 – pomalý): paralaxa, bitmapa s 6 bitplány (64 barvami, indexy 0–63).
- Siluety stromů (scroll X2 – střední): tile mapa s tmavými větvemi, 16 barev (indexy 64–79).
- Postavy, domy, objekty (scroll X3 – nejrychlejší): světlé prvky vepředu, indexy 80–127.
Siluety nejsou úplně vpředu – spíš fungují jako kontrastní vrstva mezi světelným pozadím a akcí. A protože má každá vrstva vlastní bitplán i rozsah barev, můžu je kreslit bez masek. Jen v přesném pořadí – a s jistotou, že si navzájem nešlapou na pixely.
Žádná magie. Jen dohoda mezi vrstvami, kdo kde bude dýchat.
Jak jsem hledal 64 barev lesa
Když jsem se rozhodl věnovat celých 6 bitplánů jen pozadí, bylo jasné, že těch 64 barev nemůžu vybírat ručně. Nešlo jen o to, že by to bylo pomalé – hlavně by to nevedlo k výsledku, který hledám.
Chtěl jsem, aby barvy dávaly smysl jako celek. Aby vytvářely světlo, stín, přechody, hloubku – a přitom si zachovaly rytmus a jednotu. A hlavně: aby fungovaly na Amize. Bez ditheringu, bez artefaktů. Tak, jak to zvládne blitter.
Zůročilo se velmi intezivní vzdělávání kolem AI a statistiky, kde symbióza s ním v podobě parťáka pomohla i zde. Tentokrát ale ne ve formě obrázků, ale ve formě kódu. Začal jsem si psát vlastní Python skripty, které uměly porovnávat různé metody redukce barev. Nešlo mi jen o co nejnižší chybu – ale i o estetiku výsledku. O to, jestli paleta dýchá, nebo drhne. A ze začátku drhla hodně.
Níže je pár tabulek, které jsem si během procesu vytvořil. A také kompletní kód v jazyce Python, který mi pomohly tu paletu vůbec najít. Třeba se ti budou hodit.[H1]
Věcný přechod mezi ProMotion a vlastním kódem
Než jsem se pustil do skriptování, zkoušel jsem paletu redukovat klasicky – v ProMotion NG. Jeho optimalizace je solidní, ale výsledek pořád nebyl ono. Některé detaily mizely – jak v pozadí, tak v popředí. Dithering se ukázal jako slepá cesta. Místo jemného přechodu přinesl chaos. Vzory začaly rušit. Atmosféra zmizela. Všechno působilo mrtvě a ploše.
Začal jsem přemýšlet, proč to tak je. Lidské oko přece nevnímá obraz rovnoměrně. V popředí si všímáš ostrých hran a detailů. Ale v dálce? Tam vnímáš spíš rytmus a zamlženou náladu. Jenže algoritmus v ProMotion tohle neví. Nezajímá ho mlha, světlo, ani to, kde končí stín. Jen srovnává hodnoty a hledá průměr.
A tak jsem začal hledat jiný přístup. Paletu, která by respektovala prostor – ne jen barvy. A právě v tom mi začaly pomáhat vlastní skripty.
Perceptuální histogramová kvantizace
Redukovat barvy je snadné. Zachovat světlo mezi nimi – to je ta výzva.
Představ si, že vezmeš celý obraz a podíváš se, kolik pixelů má jaký jas. Každý pixel podle toho zařadíš do krabičky – tmavé vlevo, světlé vpravo. Vznikne hřeben: tam, kde je hodně tmavých míst, sloupec vyskočí nahoru. Kde je obraz prázdnější, sloupec je nízký. To je histogram – mapa toho, kolik světla je v obraze a kde.
Co to vlastně znamená „kvantizace“?
Kvantizace je proces, při kterém zmenšíš počet možných hodnot, aby se s nimi dalo snáz pracovat. V případě barev to znamená:
máš 172 tis. odstínů – ale chceš jich použít jen 64.
Když kvantizuješ obraz, každý pixel se převede na nejbližší barvu z nové zjednodušené palety. Čím chytřeji tuhle paletu vybereš, tím lépe obraz vypadá – i když má míň barev.
Poznámka: K-means je jen jedna z mnoha metod kvantizace. Patří mezi základní nástroje strojového učení bez učitele. Neučí se z odpovědí, al ehledá vzory - a v našem případě mu předložíme barvy a on se je pokusí shrnout do 64 skupin. Existují i rychlejší nebo jednodušší přístupy, jako „median cut“ nebo „octree“, ale většina z nich pracuje jen s barvami samotnými – bez ohledu na to, kde v obraze se nachází. V tomto článku ukazuju hlavně ty, které se dají napojit na vnímání oka a kompozici scény.
Vzniklo to takhle:
„No, bude potřeba nejdřív pořešit velmi citlivou, inteligentní redukci barev. Když se barvy redukují klasicky, pixely se často začnou slévat do větších, nevzhledných ploch. Mohli bychom to obejít chytřejší redukcí barev.
Na začátek programu, který už máme na tvorbu paralaxy, bychom mohli dát proměnnou – počet požadovaných barev.
Tak, a teď tam asi nastupuje nějaký statistický model. Jednoduchá operace nad paletou. Ne průměr. Něco chytřejšího. Něco, co tu paletu neohne násilím, ale rozumí jí.
Teoreticky bychom mohli zkusit dithering, ale všiml jsem si, že to je pro oči nepříjemné. Moc chaosu. Moc vzorů. Atmosféra v háji.
Možná medián. A co kdybychom ustřihli okraje mediánu? Ty extrémy, co kazí právě tu oblast, kterou chceme sledovat. Medián je jako gaussova křivka – uprostřed kopec, po stranách svahy. A my chceme dát víc barev tam, kde je ten kopec nejvyšší. A směrem ke krajům ať jich ubývá.
Takhle nějak by to mohlo fungovat…“
Tenhle proud myšlenek mě nakonec dovedl ke konkrétním krokům:
- Konverze RGB do barevného prostoru CIELAB, který respektuje lidské vnímání.
- Výpočet histogramu složky L (jas), případně i a/b (barevný kontrast).
- Oříznutí 5–10 % krajních hodnot – extrémů, které vizuálně ruší.
- Nerovnoměrná alokace 64 barev podle hustoty histogramu.
- Clipping okrajů – tedy namapování všech hodnot mimo hlavní rozsah na krajní barvy.
A výsledek?
Atmosféra zůstala. Plynulé přechody. Ticho mezi stromy. Žádné pruhy. Žádné vzory. Jen světlo a stín.
Následující ukázka je v jazyce Python. Pro přehlednost je zobrazená pomocí tagu [PHP], který zvýrazňuje jiný jazyk, ale zachovává formátování. Pro spuštění potřebuješ knihovny PIL, NumPy, matplotlib, sklearn a OpenCV.
PHP Code:
# === PERCEPTUÁLNÍ HISTOGRAMOVÁ KVANTIZACE ===
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
import cv2
# Načtení obrázku
img_path = r"C:\Users\user\Downloads\BG2 - Copy_x256.png"
img = Image.open(img_path).convert("RGB")
img_np = np.array(img)
# Převod do LAB barevného prostoru
img_lab = cv2.cvtColor(img_np, cv2.COLOR_RGB2LAB)
lab_reshaped = img_lab.reshape((-1, 3))
# Získání L kanálu (jas) a určení hlavního rozsahu pomocí percentilů
L_channel = lab_reshaped[:, 0]
lower_percentile = np.percentile(L_channel, 5)
upper_percentile = np.percentile(L_channel, 95)
mask = (L_channel >= lower_percentile) & (L_channel <= upper_percentile)
filtered_lab = lab_reshaped[mask]
# Kvantizace v hlavním rozsahu na 64 barev
kmeans = KMeans(n_clusters=64, random_state=42)
kmeans.fit(filtered_lab)
labels = kmeans.predict(lab_reshaped)
quantized = kmeans.cluster_centers_[labels].astype(np.uint8)
# Rekonstrukce a převod zpět do RGB
quantized_lab = quantized.reshape(img_lab.shape)
quantized_rgb = cv2.cvtColor(quantized_lab, cv2.COLOR_LAB2RGB)
# Zobrazení originálu vs. zredukovaný
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
axs[0].imshow(img_np)
axs[0].set_title("Původní BG2.png")
axs[0].axis("off")
axs[1].imshow(quantized_rgb)
axs[1].set_title("Redukováno na 64 barev bez ditheringu")
axs[1].axis("off")
# Uložení vedle sebe jako porovnání
fig.savefig("redukce64_x256.png", bbox_inches="tight", pad_inches=0.0)
# Uložení jen redukovaného obrázku zvlášť
Image.fromarray(quantized_rgb).save("redukce64_only_x256.png")
# Zobrazení
plt.show()
Vypadalo to, že je hotovo.
Ale pak přišla studená sprcha přímo na mýtince.
Při prvních testech se ukázalo, že i když paleta působí na první pohled harmonicky, některé detaily zanikají. Obzvlášť v místech, kde měly být lehce odlišné světelné vrstvy – mlha, stíny, časné ranní paprsky. Redukce nedokázala rozlišit, co je jen odstín a co je kompozičně důležité.
Algoritmus dýchal – ale nerozuměl. Neviděl, kde má zůstat ostrost a kde se má barva rozplynout. A tak začalo další kolo hledání.
Kontrastně vážená paleta
Někdy nejde o to, kolik barev máš – ale kde je použiješ.
Nejdřív jsem nechal Sobelův operátor vytvořit kontrastní mapu – tedy obrázek, kde světlé body znamenají výrazné přechody (hrany, detaily) a tmavé body klid (plochy bez změn).
Tuhle mapu jsem pak nepoužil jen jako ilustraci – ale jako váhu. Každému pixelu jsem přiřadil důležitost: čím větší kontrast, tím větší šance, že ovlivní výslednou paletu.
Když pak nastoupil algoritmus k-means a začal hledat 64 nejdůležitějších barev, nedíval se na všechny pixely stejně. Díval se víc tam, kde se něco děje. A právě díky tomu jsem poprvé získal paletu, která poslouchá – ne papírově, ale očima.
Tenhle skript mi umožnil poprvé vidět, že barvy „rozložené podle očí“ opravdu dávají jiný výsledek.
Pro úplnost: redukujeme opět přibližně 172 tisíc barev na 64. V ukázce níže je to ale zjednodušeno – ze 256 na 64 – kvůli omezení formátu .gif, aby bylo možné rozdíly snadno vizuálně porovnat.
Následující ukázka je v jazyce Python. Pro přehlednost je zobrazená pomocí tagu [PHP], který sice zvýrazňuje jiný jazyk, ale uchová formátování. Pro spuštění budeš potřebovat knihovny PIL, NumPy, OpenCV, SciPy a matplotlib.
PHP Code:
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
import cv2
from scipy.ndimage import sobel
# Načtení obrázku
img_path = r"C:\Users\User\Downloads\BG2 - Copy_x256.png"
img = Image.open(img_path).convert("RGB")
img_np = np.array(img)
# Převod do LAB
img_lab = cv2.cvtColor(img_np, cv2.COLOR_RGB2LAB)
lab_reshaped = img_lab.reshape((-1, 3))
# Výpočet kontrastní mapy pomocí Sobelova operátoru (na L-kanálu)
L_channel = img_lab[:, :, 0].astype(np.float32)
sobel_x = sobel(L_channel, axis=1)
sobel_y = sobel(L_channel, axis=0)
gradient_magnitude = np.hypot(sobel_x, sobel_y)
# Normalizace gradientu do rozsahu 0–1
weight_map = gradient_magnitude / (gradient_magnitude.max() + 1e-6)
weight_map_flat = weight_map.flatten()
# Vážený výběr pixelů pro k-means trénink
num_samples = 50000
probabilities = weight_map_flat / weight_map_flat.sum()
sample_indices = np.random.choice(len(lab_reshaped), size=num_samples, p=probabilities)
weighted_lab_samples = lab_reshaped[sample_indices]
# K-means trénink
kmeans = KMeans(n_clusters=64, random_state=42)
kmeans.fit(weighted_lab_samples)
labels = kmeans.predict(lab_reshaped)
quantized = kmeans.cluster_centers_[labels].astype(np.uint8)
# Rekonstrukce a převod zpět do RGB
quantized_lab = quantized.reshape(img_lab.shape)
quantized_rgb = cv2.cvtColor(quantized_lab, cv2.COLOR_LAB2RGB)
# Zobrazení výsledku
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
axs[0].imshow(img_np)
axs[0].set_title("Původní BG2.png")
axs[0].axis("off")
axs[1].imshow(quantized_rgb)
axs[1].set_title("Redukce pomocí kontrastních vah")
axs[1].axis("off")
# Uložení vedle sebe jako porovnání
fig.savefig("redukce64_kontrVahou_x256.png", bbox_inches="tight", pad_inches=0.0)
# Uložení jen redukovaného obrázku zvlášť
Image.fromarray(quantized_rgb).save("redukce64_only_kontrVahou_x256.png")
plt.show()
Výběr pixelů podle kontrastní váhy místo náhody změnil rozložení celé scény – a poprvé jsem měl pocit, že výsledná paleta slyší, co jí říkám. Ne každá barva má být stejně důležitá.
Hybridní metoda: to nejlepší z obou světů
Jedna metoda viděla světlo. Druhá viděla hrany. Ale les potřebuje obojí.
A tak vznikla myšlenka: proč je nezkombinovat?
Proč nepoužít histogram k výběru toho, co v obraze tvoří náladu – a zároveň nechat kontrastní mapu určit, kde je třeba zachovat ostré přechody?
PHP Code:
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
import cv2
from scipy.ndimage import sobel
# Načtení obrázku BG2
img = Image.open(r"C:\Users\user\Downloads\BG2 - Copy_x256.png").convert("RGB")
img_np = np.array(img)
img_lab = cv2.cvtColor(img_np, cv2.COLOR_RGB2LAB)
lab_reshaped = img_lab.reshape((-1, 3))
# ---------- MEDIÁN VZORKOVÁNÍ ----------
L_channel = lab_reshaped[:, 0]
lower = np.percentile(L_channel, 5)
upper = np.percentile(L_channel, 95)
mask_median = (L_channel >= lower) & (L_channel <= upper)
median_lab = lab_reshaped[mask_median]
# Náhodný výběr z mediánu
num_median = 35000
median_sample = median_lab[np.random.choice(len(median_lab), size=num_median, replace=False)]
# ---------- KONTRASTNÍ VZORKOVÁNÍ ----------
L_image = img_lab[:, :, 0].astype(np.float32)
sobel_x = sobel(L_image, axis=1)
sobel_y = sobel(L_image, axis=0)
gradient_magnitude = np.hypot(sobel_x, sobel_y)
weight_map = gradient_magnitude / (gradient_magnitude.max() + 1e-6)
probabilities = weight_map.flatten() / weight_map.sum()
num_contrast = 15000
contrast_indices = np.random.choice(len(lab_reshaped), size=num_contrast, p=probabilities)
contrast_sample = lab_reshaped[contrast_indices]
# ---------- SLOUČENÍ A K-MEANS ----------
hybrid_samples = np.vstack((median_sample, contrast_sample))
kmeans = KMeans(n_clusters=64, random_state=42)
kmeans.fit(hybrid_samples)
labels = kmeans.predict(lab_reshaped)
quantized = kmeans.cluster_centers_[labels].astype(np.uint8 )
# Výstup
quantized_lab = quantized.reshape(img_lab.shape)
quantized_rgb = cv2.cvtColor(quantized_lab, cv2.COLOR_LAB2RGB)
# Zobrazení výsledku
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
axs[0].imshow(img_np)
axs[0].set_title("Původní BG2.png")
axs[0].axis("off")
axs[1].imshow(quantized_rgb)
axs[1].set_title("Hybridní redukce (70% medián + 30% kontrast)")
axs[1].axis("off")
# Uložení vedle sebe jako porovnání
fig.savefig("redukce64_hybrid_x256.png", bbox_inches="tight", pad_inches=0.0)
# Uložení jen redukovaného obrázku zvlášť
Image.fromarray(quantized_rgb).save("redukce64_onl y_hybrid_x256.png")
plt.show()
Doporučuji se soutředit na detaily před a po u stromů, kmenů a obrysy korun kousek nad pravou částí mýtinky.
Hybridní metoda zostřila kontury – obrysy stromů, kmeny i větve dostaly jistotu a řád. Jako by někdo napnul plátno, které se předtím mírně vlnilo. Vzadu se ale něco změnilo: horizont ztratil drobné šumy, jeho textura se rozplynula. A právě tím získal to podstatné – klid. Protože když se díváš do dálky, nevnímáš už jednotlivé detaily. Vnímáš, jestli scéna dýchá. A tahle začala.
Shannonova entropie
Napadla mě zvláštní myšlenka.
Co kdybych nevybíral pixely podle jasu, ani podle kontrastu – ale podle míry překvapení?
Ne, ten autor článku je blázen.
Ale právě tahle myšlenka mě zavedla dál. Co když v obraze existují oblasti, které jsou pro naše vnímání nečekané – a právě proto si zaslouží větší pozornost? Oblasti, kde se děje něco neobvyklého. Něco, co by algoritmus normálně přehlédl.
A tak jsem si vzal na pomoc nástroj z teorie informace: Shannonovu entropii.
Původně vznikla jako způsob, jak měřit množství informace ve zprávě – ale tady ji použiju jinak. Jako mapu překvapení. Každému místu v obraze přiřadím hodnotu podle toho, jak moc se liší od svého okolí. A čím víc se liší, tím větší má „informační váhu“.
Tady už se nedíváme jen na světlo nebo kontrast. Díváme se na pravděpodobnost. Na to, jak moc je nějaký obrazový prvek statisticky nepravděpodobný – a tím i vizuálně výrazný.
A najednou to začne dávat smysl. Ne „hlasitá“ metoda, která zvýrazní všechno. Ale jemná sonda, která vytahuje z obrazu to, co opravdu zaujme. Ne oči. Ale pozornost.
[/QUOTE]
PHP Code:
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import cv2
from sklearn.cluster import KMeans
from numba import jit
# Optimalizovaná funkce pro výpočet lokální entropie na L-kanálu
@jit(nopython=True)
def compute_entropy_map_fast(image_gray, window_size=5):
h, w = image_gray.shape
pad = window_size // 2
entropy_map = np.zeros((h, w), dtype=np.float32)
for y in range(pad, h - pad):
for x in range(pad, w - pad):
# Extrakce lokálního okna
window = image_gray[y - pad:y + pad + 1, x - pad:x + pad + 1].flatten()
# Histogram
hist = np.zeros(256, dtype=np.int32)
for val in window:
hist[int(val)] += 1
# Normalizace histogramu
total = window.size
prob = hist / total
# Výpočet entropie
entropy = 0.0
for p in prob:
if p > 0:
entropy -= p * np.log2(p)
entropy_map[y, x] = entropy
return entropy_map
# Načtení a převod do LAB
img = Image.open(r"C:\Users\users\Downloads\BG2 - Copy_x256.png").convert("RGB")
img_np = np.array(img)
img_lab = cv2.cvtColor(img_np, cv2.COLOR_RGB2LAB)
lab_reshaped = img_lab.reshape((-1, 3))
# Výpočet entropie z L-kanálu
L_image = img_lab[:, :, 0].astype(np.uint8)
entropy_map = compute_entropy_map_fast(L_image, window_size=5)
# Normalizace
entropy_map_norm = entropy_map / (entropy_map.max() + 1e-6)
entropy_flat = entropy_map_norm.flatten()
# Výběr pixelů podle entropie
num_samples = 50000
entropy_probabilities = entropy_flat / entropy_flat.sum()
entropy_indices = np.random.choice(len(lab_reshaped), size=num_samples, p=entropy_probabilities)
entropy_samples = lab_reshaped[entropy_indices]
# K-means kvantizace
kmeans = KMeans(n_clusters=64, random_state=42)
kmeans.fit(entropy_samples)
labels = kmeans.predict(lab_reshaped)
quantized = kmeans.cluster_centers_[labels].astype(np.uint8)
# Výstup
quantized_lab = quantized.reshape(img_lab.shape)
quantized_rgb = cv2.cvtColor(quantized_lab, cv2.COLOR_LAB2RGB)
# Zobrazení
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
axs[0].imshow(img_np)
axs[0].set_title("Původní BG2.png")
axs[0].axis("off")
axs[1].imshow(quantized_rgb)
axs[1].set_title("Redukce podle entropie (L-kanál, optimalizováno)")
axs[1].axis("off")
# Uložení vedle sebe jako porovnání
fig.savefig("redukce64_entropy_x256.png", bbox_inches="tight", pad_inches=0.0)
# Uložení jen redukovaného obrázku zvlášť
Image.fromarray(quantized_rgb).save("redukce64_only_entropy_x256.png")
plt.show()
„Entropická metoda jako by nehledala okázalé detaily, ale spíš drobné anomálie – ty nenápadné odchylky, které činí obraz zajímavým. Je to jiný druh citu. Místo světla nebo hrany slyší spíš šepot struktur.
Je vidět lehké zotření tvaru korun, ale ne tak výrazné jako u hybridu. Stíny mezi stromy v popředí jsou prokreslenější – a tím i hlubší a detailnější. Také horizont se o něco víc zklidnil. Oproti hybridu působí tišeji.“
Jak vytvořit plynulé opakování vrstvy pro paralaxu
Obraz, který se má plynule posouvat, nesmí mít začátek ani konec. Musí být připravený navazovat – beze švů.
Šlo by to řešit ručně, například tím, že na sebe okraje dokreslíš. Jenže u přirozených scenérií jako je les, tráva nebo mlha, tohle moc nefunguje. Proto jsme použili elegantnější řešení: automatické vytvoření prolínací smyčky. Tři obrazy, dvě zóny, jedna plynulá dlaždice
Naše metoda využívá tři opakování výchozího obrázku (dlaždice 1, 2 a 3) a mezi nimi vytváří dvě prolínací zóny (blendingy). Přesněji:
- Vložíme dlaždici 1 (např. 512 pixelů širokou).
- Hned za ni přidáme prolínání (blending) s dlaždicí 2 – tedy její levý okraj se začne „míchat“ s pravým okrajem dlaždice 1.
- Následuje celá dlaždice 2, za ní opět blending s dlaždicí 3.
- Nakonec ořízneme obraz tak, že začátek i konec nové dlaždice vznikají uvnitř blendingových oblastí.
- Začátek i konec výsledné dlaždice jsou plynulé – protože vznikly právě v místě, kde se obraz prolíná.
- Když se během hry scrollovací offset posune zpět na nulu (což se na Amize děje fyzicky), obraz „neskočí“ – jen naváže.
Code:
[dlaždice 1] → [blending 1-2] → [dlaždice 2] → [blending 2-3] → [dlaždice 3] ↑ ↑ začátek výřezu konec výřezu
Autor článku si teď trochu drbe hlavu, zda to opravdu udělal tak, jak napsal. Vypadá to až příliš dobře.
PHP Code:
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
# Načtení původního obrázku
input_path = r"C:\Users\user\Downloads\BG2.png"
img = Image.open(input_path).convert("RGB")
w, h = img.size
# Parametry
tile_count = 4 # 4 segmenty pro 3 bezešvé výřezy
overlap = int(w * 0.1)
# Výpočet celkové šířky
total_width = w + (tile_count - 1) * (w - overlap)
canvas = np.zeros((h, total_width, 3), dtype=np.uint8)
# První segment bez blendingu
canvas[:, :w] = np.array(img)
# Pokládání a blending zleva doprava
for i in range(1, tile_count):
x_start = i * (w - overlap)
x_blend_start = x_start - overlap
segment = np.array(img)
# Prolínací oblast
for x in range(overlap):
alpha = x / (overlap - 1)
canvas[:, x_blend_start + x] = (
canvas[:, x_blend_start + x] * (1 - alpha) + segment[:, x] * alpha
).astype(np.uint8)
# Zbytek segmentu
blend_end = x_start + (w - overlap)
canvas[:, x_start:blend_end] = segment[:, overlap:]
# Oříznutí pouze části: dlaždice 1 až 3 (tedy 3 efektivní tiles včetně blend začátku i konce)
x1 = (w - overlap) * 1
x2 = x1 + 2 * (w - overlap)
final_img = canvas[:, x1:x2]
# Zobrazení a uložení bezešvého výsledku
plt.figure(figsize=(22, 6))
plt.imshow(final_img)
plt.axis("off")
plt.savefig("testv2.png", bbox_inches='tight', pad_inches=0.0)
plt.show()
Přehled použitých metod pro redukci barev
Některé jsem tu neuvedl, aby text článku nebyl příliš dlouhý
🎨 Srovnání metod redukce barev (praktické pozorování)
Standardní median cut / k-means | ✅ Rychlá a široce podporovaná metoda ✅ Výsledky často dostačující pro fotografie |
❌ Ztrácí jemné nuance ❌ Vede ke splývání oblastí s podobnou barvou ❌ Nevhodné pro pixel art a stylizaci s jemnými stíny |
Dithering (Floyd–Steinberg apod.) | ✅ Výrazné zachování dojmů z detailů ✅ Zvyšuje vizuální kontrast a barevnou pestrost při omezené paletě |
❌ Působí chaoticky nebo zrnitě ❌ Oči se unavují při sledování organických tvarů ❌ Narušuje klid scén s atmosférou |
Hybridní redukce (variance + medián) | ✅ Zachovává kontrasty i tiché přechody ✅ Neničí listy, větve ani textury ✅ Udržuje konzistenci detailů v popředí i pozadí |
🔸 Může zvýraznit šum ve zdroji 🔸 Někdy nutné ručně doladit okraje objektů |
Redukce s kontrastní vahou | ✅ Zviditelňuje popředí a siluety ✅ Dobře modeluje kontury a dramatické osvětlení |
❌ Ztrácí jemné odstíny ve vzdáleném pozadí ❌ Může zvýraznit nechtěné detaily v tmavých oblastech |
Redukce s Shannonovou entropií (L-kanál) | ✅ Skvělé rozlišení struktur napříč obrazem ✅ Vysoká věrnost detailům v popředí i pozadí ✅ Funguje přirozeně i při nízkém rozlišení |
🔸 Výpočetně náročnější (ale přijatelně) 🔸 Malé rozdíly v nevýrazných oblastech mohou být vyhlazeny |
Entropie v plném LAB prostoru | ✅ Jemnější záchyt barevných kontrastů ✅ Respektuje složité barevné přechody |
❌ Méně čitelné výsledky ❌ Ztrácí některé kontury oproti L-kanálu ❌ Vizuálně působí „měkčeji“, méně pixelově přesně |
Když už zmenšit, tak s citem
A tak jsem začal zkoumat úplně jinou oblast. Ne jak vybrat 64 barev – ale jak připravit těch několik stovek tisíc pixelů, které tu volbu ovlivní.
A co když obraz, který dávám algoritmu na vstup, vůbec není ideální?
Ve všech předchozích pokusech jsem redukoval barvy až na hotovém obraze. Ale co když už samotný resize mění rytmus, ostrost i distribuci detailů takovým způsobem, že tím pozdější výběr barev nevyhnutelně zkresluje?
A právě tady se začíná rodit scéna. Ne v barvách, ale v proporcích, texturách a rytmech. Protože downscaling (zmenšení měřítka) není jen technický mezikrok – je to první forma kompozice. A když ho uděláš dobře, může posloužit jako citlivý základ pro další fáze – ale musí přijít jako první. Shannonova entropie, nebo jakákoli jiná metoda, bude fungovat dobře jedině tehdy, pokud dostane zdravý vstup.
Ne každý pixel přežije cestu.
Následují různé metody zmenšení velikosti obrázku (downscaling).
Hybridní downscale: když chceš čistotu i ostrost
Při zmenšování obrázku pro Amigu jsem narazil na známý problém: některé metody rozmažou detaily, jiné je přeostří a vytvoří artefakty. Ale co když chci obojí – jemnost plošných oblastí a zároveň ostré hrany tam, kde na nich záleží?
A tak jsem vytvořil vlastní metodu: hybridní downscale.
Jak to funguje:
- Obraz rozdělím na bloky podle cílového rozlišení.
- Každý blok analyzuji:
- Pokud má nízkou varianci (tedy je klidný a bez detailu), použiji průměr – výsledek je měkký a přirozený.
- Pokud má vysokou varianci (obsahuje detaily, hrany), použiji medián – který udrží ostré přechody bez halo efektu.
Průměr i medián mají své výhody – ale každá z nich selhává v opačném kontextu. Tím, že se rozhodnutí dělá na úrovni jednotlivých bloků, může obraz zůstat jemný tam, kde to má smysl, a zároveň ostrý tam, kde je potřeba struktura.
Praktické porovnání
Pro srovnání jsem zvolil i běžně doporučovaný „faster resize“ z programu IrfanView, který je oblíbený pro pixel art kvůli své ostrosti. A výsledky?
- Hybridní downscale dokázal lépe udržet plynulé přírodní křivky – větve, listí, oblé tvary. Přechody jsou měkčí a přirozenější.
- Přesto kontury vzdálených vrstev lesa zůstaly přesné – bez toho, aby působily roztřeseně nebo neostře.
- Některé kmeny stromů jsou o pixel silnější, ale působí uvěřitelněji – nejsou přeostřené, ale pevné.
- Naopak „faster resize“ občas vytvářel tzv. barevné echo – přechodové pixely měly nežádoucí odstín, jako by kolem křivky zůstal duch.
Výsledek? Hybridní metoda je věcnější, ale přitom přirozenější. Nezvýrazňuje věci, které do pozadí nepatří, a zároveň neničí to, co má tvořit hloubku.
Lanczos se samplingem
Zkoušel jsem i Lanczor z IrfanView se samplingem, ale ten celkově obraz rozmaže, což je dost špatný výsledek
Retinex / CLAHE: když chceš vytáhnout světlo ze stínu
Jedním z přístupů, jak zvýraznit detaily v obraze, je metoda z oblasti počítačového vidění zvaná Retinex. Vychází z principu, jak lidské oko vnímá světlo – ne absolutně, ale ve vztahu ke svému okolí. V praxi to znamená, že tmavé oblasti se mohou „rozsvítit“, aniž by se přepálilo světlé pozadí.
Jednou z jeho běžně používaných forem je CLAHE – tedy Contrast Limited Adaptive Histogram Equalization. Jak CLAHE funguje:
- Rozdělí obraz na malé bloky (dlaždice).
- V každé z těchto oblastí rovnoměrně rozprostře histogram jasu – to znamená, že i malé rozdíly ve stínech se stanou viditelnými.
- Omezí přepaly tím, že nepustí kontrast příliš vysoko (contrast limiting).
- Přechody mezi bloky se hladce spojí, aby nevznikly hranice.
✦ Laplacian Enhancement ✦
Technický zázrak skrytý v jemnosti
Když ostré nemusí být hrubé
Už jsme zkoušeli vše: průměry, mediány, entropii, varianci i histogramy. Každá metoda něco zvýraznila – a něco ztratila. A pak přišel Laplacian. Nenápadný. Tichý. Obyčejný filtr, který bývá přehlížen. Ale něco v něm bylo jinak.
Na první pohled obraz jen mírně projasní. Nic dramatického. Jenže pak… pak se podíváš blíž. Vidět to, co už tam bylo
Tohle není ostření jako v grafických editorech. Žádné tvrdé hrany. Žádné přepaly. Místo toho Laplacian zvýrazní přirozený rytmus. Rozvlní přechody světla, přetáhne je do tenké vrstvy navíc – a nechá je vrátit zpátky do obrazu. Ne mění, ale připomíná. Přidám sem některé mé bezprostřední reakce jako citace
„Z levé strany korun stromů se některé listí dostaly do jemných detailů, které předtím vůbec nebyly vidět.“
Jedna z největších předností Laplacianu je v jeho střídmosti. Nepřehání to. Respektuje původní obraz. Zachová atmosféru, ale podtrhne, co bylo předtím tiché. A díky tomu vznikne výsledek, který není jen ostřejší – je přítomnější.
„Síla efektu je akorát. Projasní se větve. A zůstane světlo.“
Tahle metoda má zvláštní schopnost: ukázat víc, aniž by změnila smysl. Není to trik. Není to podvod. Je to nástroj, který připomíná, že v dobrém obrazu je vždycky něco navíc – jen čeká, až to někdo probudí.
PHP Code:
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import cv2
from scipy.ndimage import variance
from skimage.util import view_as_blocks
from skimage.filters.rank import entropy
from skimage.morphology import disk
from skimage.color import rgb2gray
from skimage.util import img_as_ubyte
def apply_laplacian_enhancement(image: Image.Image, alpha: float = 0.5) -> Image.Image:
"""
Aplikuje Laplaceův filtr pro zvýraznění hran a přidá je zpět do původního obrazu.
`alpha` určuje sílu efektu (doporučeno 0.3 až 0.7).
"""
img_np = np.array(image.convert("RGB"), dtype=np.uint8)
# Převod na šedotónový obrázek
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
# Aplikace Laplaceova filtru
laplacian = cv2.Laplacian(gray, cv2.CV_16S, ksize=3)
laplacian = cv2.convertScaleAbs(laplacian)
# Vytvoření barevného "maskovacího" obrazu
laplacian_colored = cv2.merge([laplacian] * 3)
# Kombinace původního obrazu s maskou hran
enhanced = cv2.addWeighted(img_np, 1.0, laplacian_colored, alpha, 0)
return Image.fromarray(enhanced)
def intelligent_downscale_hybrid(image: Image.Image, target_height: int = 256, var_threshold: float = 500.0) -> Image.Image:
"""
Inteligentní downscaling kombinující medián a průměr na základě lokální variance.
- Používá medián v detailech (vysoká variance), průměr jinde.
"""
img_np = np.array(image.convert("RGB"))
h, w, _ = img_np.shape
scale_ratio = target_height / h
target_width = int(w * scale_ratio)
# Rozměr výstupního obrazu
out_img = np.zeros((target_height, target_width, 3), dtype=np.uint8)
# Výpočet velikosti bloků v originále
block_h = h // target_height
block_w = w // target_width
for y in range(target_height):
for x in range(target_width):
y0 = y * block_h
x0 = x * block_w
block = img_np[y0:y0 + block_h, x0:x0 + block_w]
var_val = np.var(block) # celková variance v RGB bloku
if var_val >= var_threshold:
pixel_val = np.median(block.reshape(-1, 3), axis=0)
else:
pixel_val = np.mean(block.reshape(-1, 3), axis=0)
out_img[y, x] = pixel_val.astype(np.uint8)
return Image.fromarray(out_img)
def intelligent_downscale_hybrid_entropy(image: Image.Image, target_height: int = 256, var_threshold: float = 500.0, entropy_weight: float = 0.5) -> Image.Image:
"""
Inteligentní downscaling s hybridním přístupem:
- Používá průměr nebo medián na základě lokální variance.
- Navíc zvýhodňuje detaily na základě entropie.
"""
img_np = np.array(image.convert("RGB"))
h, w, _ = img_np.shape
scale_ratio = target_height / h
target_width = int(w * scale_ratio)
# Entropie v šedotónovém prostoru
gray = img_as_ubyte(rgb2gray(img_np))
entropy_map = entropy(gray, disk(3))
# Výstupní obraz
out_img = np.zeros((target_height, target_width, 3), dtype=np.uint8)
block_h = h // target_height
block_w = w // target_width
for y in range(target_height):
for x in range(target_width):
y0 = y * block_h
x0 = x * block_w
block = img_np[y0:y0 + block_h, x0:x0 + block_w]
var_val = np.var(block)
ent_val = np.mean(entropy_map[y0:y0 + block_h, x0:x0 + block_w])
# Kombinovaná váha rozhodování
score = (1 - entropy_weight) * var_val + entropy_weight * ent_val * 1000 # škálujeme entropii
if score >= var_threshold:
pixel_val = np.median(block.reshape(-1, 3), axis=0)
else:
pixel_val = np.mean(block.reshape(-1, 3), axis=0)
out_img[y, x] = pixel_val.astype(np.uint8)
return Image.fromarray(out_img)
# Načtení původního obrázku
input_path = r"C:\Users\rysavmar\Downloads\BG2.png"
img = Image.open(input_path).convert("RGB")
w, h = img.size
# Parametry
tile_count = 4 # 4 segmenty pro 3 bezešvé výřezy
overlap = int(w * 0.1)
# Výpočet celkové šířky
total_width = w + (tile_count - 1) * (w - overlap)
canvas = np.zeros((h, total_width, 3), dtype=np.uint8)
# První segment bez blendingu
canvas[:, :w] = np.array(img)
# Pokládání a blending zleva doprava
for i in range(1, tile_count):
x_start = i * (w - overlap)
x_blend_start = x_start - overlap
segment = np.array(img)
# Prolínací oblast
for x in range(overlap):
alpha = x / (overlap - 1)
canvas[:, x_blend_start + x] = (
canvas[:, x_blend_start + x] * (1 - alpha) + segment[:, x] * alpha
).astype(np.uint8)
# Zbytek segmentu
blend_end = x_start + (w - overlap)
canvas[:, x_start:blend_end] = segment[:, overlap:]
# Oříznutí pouze části: dlaždice 1 až 3 (tedy 3 efektivní tiles včetně blend začátku i konce)
x1 = (w - overlap) * 1
x2 = x1 + 2 * (w - overlap)
final_img = canvas[:, x1:x2]
# sekce pro intelligent hybrid downscale obrázku
img_for_downscale = Image.fromarray(final_img)
# ✳️ Aplikace Laplacian enhancementu
img_enhanced = apply_laplacian_enhancement(img_for_downscale, alpha=0.5)
# zavolání hybridního downscalingu
downscaled = intelligent_downscale_hybrid(img_enhanced, target_height=256)
downscaled.save("BG2_laplace_hybrid_downscaled.png")
# Zavolání hybridního downscalingu s entropií - zjištěno nemá vliv, entropie je příliš dobře rozložená
downscaled2 = intelligent_downscale_hybrid_entropy(img_enhanced, target_height=256, var_threshold=100.0, entropy_weight=0.9)
downscaled2.save("BG2_laplace_hybrid_entropy_downscaled.png")
# Zobrazení a uložení bezešvého výsledku
plt.figure(figsize=(22, 6))
plt.imshow(final_img)
plt.axis("off")
plt.savefig("testv2.png", bbox_inches='tight', pad_inches=0.0)
plt.show()
🔍 Srovnání metod downscalingu (praktické pozorován
Hybridní downscale (variance-aware: průměr + medián) |
✅ Přesné kontury vzdálených vrstev ✅ Přírodní, plynulé přechody bez zubatosti ✅ Bezešvé prohloubení hloubky scény ✅ Neztrácí žádné detaily |
🔸 Některé linie mohou být o 1px širší (lze ručně doladit) |
IrfanView Fast Resize | ✅ Ostré, čisté hrany ✅ Dobré pro GUI a dlaždice s velkým kontrastem |
❌ Ghost echo kolem kontrastních hran ❌ Umělý vzhled u organických tvarů ❌ ❗ Mizející tenké prvky (např. kmeny stromů) |
Lanczos (se sampling) | ✅ Dobré pro fotografie, soft blending ✅ Konzistentní interpolace |
❌ Rozmazané kontury ❌ Ztráta textury ❌ Nevhodné pro pixel art s přesně vyznačenými hranami |
Retinex (CLAHE) před downscalingem | ✅ Projasní tmavé oblasti ✅ Zvýší viditelnost v hlubokých stínech |
❌ Může způsobit nežádoucí barevné posuny (např. zeleň → modř) ❌ Nevhodné pro realistickou stylizaci |
Laplacian enhancement před downscalingem | ✅ Přidává přirozený světelný efekt na okraje ✅ Zlepšuje čitelnost větví a listí ✅ Působí jako jemné prosvícení koruny stromů |
🔸 Mírně zvýší jas (což může být i výhoda) 🔸 U velmi kontrastních scén může být potřeba doladit alpha |
✦ Závěr ✦
Obraz, který se rozhodl žít
Když se teď ohlédnu za všemi těmi skripty, testy, tabulkami a pixelovými porovnáními, uvědomuji si jednu věc: tenhle článek vlastně není jen o grafice. Je o hledání. O tom, jak se z hromady dat stane pocit. Jak se z čísel a filtrů začne klubat nálada. Jak se obraz rozhodne, že bude žít.
Nešlo mi o to najít „nejlepší“ metodu. Neexistuje jedna správná. Každá scéna potřebuje něco jiného – někde ticho, jinde ostrost. Někdy světlo, jindy stín. Ale když dáš každé vrstvě prostor, každé barvě důvod a každému filtru cit, může se stát zázrak: obraz, který nejen funguje, ale působí.
„320×256 pixelů. Několik bitplánů, omezená paleta. A přesto se dá vytvořit něco, co má náladu.“
A až přijde ten den – někdy pozdě v červnci – kdy se spustí první hratelné demo, nebude to jen ukázka pixelů. Bude to pozvání. Malý krok do lesa, který jsem hledal nejen v barvách, ale i v sobě. Pokud se zastavíš, třeba ho uslyšíš taky.