Jdi na navigaci předmětu

PGA — Krita Python Plugins (6 cvičení)

Šest praktických cvičení: od „Hello filtru“ přes konvoluce, frekvenční filtr, barevné transformace až po dávkové zpracování a publikaci pluginu. Vše s ohledem na reálná omezení Krity (BGRA, chybějící NumPy) a výkon (ROI, blokové čtení).

Pokud jste dříve pracovali s GIMPem, může vám pomoci migrační tahák zde: PGA1.adoc

1. Slidy a materiály ke stažení

MateriálOdkaz
Krita — Týden 01: Úvod & Navigace PDF
Krita — Týden 02: Barvy & Soubory (extended) PDF
Krita — Týden 03: Vrstvy/Masky/Štětce (extended) PDF
Krita — Týden 04: Retuš (masterclass) PDF
Krita — Týden 05: Engine-specific nastavení (v4) PDF
Krita — Týden 05: Games export (masterclass) PDF
Krita — Týden 06: Animace & závěr (extended) PDF
PGA — Krita Plugins: Cvičení 2 (Konvoluce I) PDF ZIP
PGA — Krita Plugins: Cvičení 3 (Konvoluce II / Gauss & Sobel) PDF ZIP
PGA — Krita Plugins: Cvičení 4 (FFT / FreqLab)PGA — Krita Plugins: Cvičení 4 (FFT / FreqLab)
PDF ZIPPGA — Krita Plugins: Cvičení 5 (ColorLab)
PGA — Krita Plugins: Cvičení 5 (ColorLab) PDF ZIP
PGA — Krita Plugins: Cvičení 6 (BatchLab)PGA — Krita Plugins: Cvičení 6 (BatchLab)

2. Před startem — prostředí, Script Starter, cesty, importy

2.1. Umístění pluginů (pykrita)

  • Windows: %APPDATA%\krita\pykrita (např. C:\Users\<username>\AppData\Roaming\krita\pykrita)
  • Linux: ~/.local/share/krita/pykrita
  • macOS: ~/Library/Application Support/Krita/pykrita (ověřte v Help → About Krita → Resources Location)

2.2. Vytvoření pluginu — Krita Script Starter (doporučeno)

  1. Settings → Configure Krita → Python Plugin Manager.
  2. Zapněte Krita Script Starter a restartujte Kritu.
  3. Tools → Scripts → Krita Script Starter → vyplňte údaje → plugin se vytvoří v pykrita/.

2.3. Vytvoření ručně (alternativa)

Struktura:

pykrita/
  my_plugin.desktop
  my_plugin/
    __init__.py
    my_plugin.py

my_plugin.desktop:

[Desktop Entry]
Type=Service
ServiceTypes=Krita/PythonPlugin
X-Python-2-Compatible=false
X-Krita-Manual=Manual.html
Name=My Plugin
Comment=This is my plugin
X-Krita-Plugin-Category=Tools
X-Krita-Enabled=true
X-Krita-Python-Module=my_plugin

my_plugin/init.py:

from .my_plugin import MyPlugin
from krita import Krita
app = Krita.instance()
extension = MyPlugin(parent=app)
app.addExtension(extension)

my_plugin/my_plugin.py (skeleton):

from krita import Extension

EXTENSION_ID = "pykrita_my_plugin"
MENU_ENTRY   = "My Plugin"

class MyPlugin(Extension):
    def __init__(self, parent):
        super().__init__(parent)
        self.app = parent

    def setup(self):
        pass  # volá se při startu Krity

    def createActions(self, window):
        action = window.createAction(EXTENSION_ID, MENU_ENTRY, "tools/scripts")
        action.triggered.connect(self.action_triggered)

    def action_triggered(self):
        # TODO: implementace
        pass

2.4. Importy a externí moduly

  • Krita (Windows) má vlastní Python → systémové balíčky (numpy, scipy…) nejsou k dispozici.
  • Když import selže, plugin se často tiše nenačte (v UI nic neuvidíte).
  • Řešení:
    • preferujte čistý Python (blokové operace, memoryview);
    • případně krita-pip (instalace balíčků do pluginu) — používat střídmě;
    • debug: spouštějte Kritu z konzole, sledujte traceback; v Python Plugin Manager kontrolujte stav pluginů.

2.5. Přístup k pixelům (BGRA) + ROI & refresh

from krita import Krita

def active_layer_and_roi():
    app = Krita.instance()
    doc = app.activeDocument()
    if not doc:
        return None, None, (0,0,0,0)
    node = doc.activeNode()
    sel = doc.selection()
    if sel:
        x,y,w,h = sel.x(), sel.y(), sel.width(), sel.height()
    else:
        x,y,w,h = 0, 0, doc.width(), doc.height()
    return doc, node, (x,y,w,h)

def invert_rgb_bgra_bytearray():
    doc, node, (x,y,w,h) = active_layer_and_roi()
    if not node or w==0 or h==0: return
    # Krita dává data jako BGRA (8bit)
    data = bytearray(node.pixelData(x, y, w, h))
    mv = memoryview(data)
    for i in range(0, len(mv), 4):
        b = mv[i+0]; g = mv[i+1]; r = mv[i+2]  # A = mv[i+3]
        mv[i+0] = 255 - b
        mv[i+1] = 255 - g
        mv[i+2] = 255 - r
    node.setPixelData(mv.tobytes(), x, y, w, h)
    doc.refreshProjection()
    # někdy pomůže toggle viditelnosti:
    # node.setVisible(False); node.setVisible(True)

PGA — Krita Python Plugins: Cvičení 1 (Hello filter: plugin, akce, BGRA, výběr a bezpečná inverze)

ZIP

Cíl: zprovoznit plugin, přidat akci do Tools/Scripts, číst/zapisovat BGRA, fungovat na výběr i celou vrstvu. Výstup: „Invert RGB“ s volbou „Apply to selection only“.

.1. Požadavky & prostředí

  • Krita 5.x+ (doporučeno 5.2+), vestavěný Python Krity.
  • Editor (VS Code / PyCharm).
  • Základy Pythonu (třídy, moduly).

.2. Vytvoření pluginu (Script Starter / ručně)

Viz „Před startem“. Po vytvoření ověřte v Settings → Configure Krita → Python Plugin Manager, že je plugin Enabled.

.3. Kostra akce a bezpečná inverze

from krita import Extension

EXTENSION_ID = "pykrita_hello_krita"
MENU_PATH    = "tools/scripts"

class HelloKrita(Extension):
    def __init__(self, parent):
        super().__init__(parent)
        self.app = parent

    def setup(self): pass

    def createActions(self, window):
        action = window.createAction(EXTENSION_ID, "Invert Colors (selection-safe)", MENU_PATH)
        action.triggered.connect(self.run_invert)

    def _active_doc_node_roi(self):
        doc = self.app.activeDocument()
        if not doc: return None, None, (0,0,0,0)
        node = doc.activeNode()
        sel  = doc.selection()
        if sel:
            x,y,w,h = sel.x(), sel.y(), sel.width(), sel.height()
        else:
            x,y,w,h = 0,0,doc.width(),doc.height()
        return doc, node, (x,y,w,h)

    def run_invert(self):
        doc, node, (x,y,w,h) = self._active_doc_node_roi()
        if not node or w==0 or h==0: return
        data = bytearray(node.pixelData(x, y, w, h))  # BGRA
        mv = memoryview(data)
        for i in range(0, len(mv), 4):
            mv[i+0] = 255 - mv[i+0]  # B
            mv[i+1] = 255 - mv[i+1]  # G
            mv[i+2] = 255 - mv[i+2]  # R
            # mv[i+3] = mv[i+3]      # A beze změny
        node.setPixelData(mv.tobytes(), x, y, w, h)
        doc.refreshProjection()
Varování:

BGRA! Zpracovávejte jen B,G,R; alfa nechte beze změny.

.4. Mini-UI (volba „pouze výběr“)

from PyQt5.QtWidgets import QDialog, QVBoxLayout, QCheckBox, QPushButton

class InvertDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Invert Colors — Options")
        self.only_selection = QCheckBox("Apply to selection only (if exists)")
        ok_btn = QPushButton("OK"); cancel_btn = QPushButton("Cancel")
        lay = QVBoxLayout(self); lay.addWidget(self.only_selection); lay.addWidget(ok_btn); lay.addWidget(cancel_btn)
        ok_btn.clicked.connect(self.accept); cancel_btn.clicked.connect(self.reject)

Napojení na akci:

from PyQt5.QtWidgets import QApplication, QDialog

def createActions(self, window):
    action = window.createAction(EXTENSION_ID, "Invert Colors (dialog)", MENU_PATH)
    action.triggered.connect(self.run_invert_dialog)

def run_invert_dialog(self):
    dlg = InvertDialog()
    if dlg.exec_() != QDialog.Accepted: return
    if dlg.only_selection.isChecked():
        doc = self.app.activeDocument()
        if doc and not doc.selection():
            return  # nic bez výběru
    self.run_invert()
    QApplication.processEvents()

.5. Odevzdání

  • Adresář pluginu (desktop, init.py, hello_krita.py)
  • Krátké README (kde v menu, jak spustit)
  • PNG/GIF „před/po“ + screenshot dialogu

.6. Checklist kvality

  • Plugin se načte (viditelný v Python Plugin Manager).
  • Akce v Tools/Scripts.
  • RGB invert (BGRA pořadí), A zachována.
  • ROI = výběr; bez výběru celé plátno.
  • refreshProjection() po zápisu.
  • Kód přehledný, komentovaný.

.7. Rubrika (0–4)

  • Funkčnost • Technická správnost • Kód & struktura • UX

.8. Časté chyby & troubleshooting

  • Akce není v menu → pád při import. Spusťte Kritu z konzole, sledujte traceback.
  • Mění se průhlednost → omylem upravujete A.
  • Pomalé → pracujte s ROI, po řádcích/blocích.
  • UI se neprokreslírefreshProjection(), případně krátké vyp/zap viditelnosti vrstvy.

PGA — Krita Python Plugins: Cvičení 2 (Konvoluce I)

Autor: tým PGA :toc: macro

ZIP :toclevels: 3 :sectnums: :icons: font :source-highlighter: rouge :experimental: :lang: cs

Cíl cvičení: vytvořit plugin ConvolutionLab s obecným konvolučním jádrem (3×3; volitelně 5×5), náhledem na ROI, volbou okrajů (clamp/mirror) a normalizací jádra. Zpracovávat jen B,G,R (BGRA), alfa zachovat. Optimalizovat průchod po řádcích/blocích.

1. Připraveno z Cvičení 1

  • Umíte vytvořit plugin přes Script Starter a registrovat akci do Tools/Scripts.
  • Znáte BGRA pořadí kanálů a práci s ROI dle výběru.
  • Ovládáte pixelData()/setPixelData() + refreshProjection().
Varování:

Bez NumPy: počítejte čistě v Pythonu. Krita na Windows používá vlastní Python – nepočítejte s numpy/scipy.

2. Zadání

Vytvořte plugin ConvolutionLab: * UI s maticí 3×3 (editovatelná), přepínač Normalize sum, Preserve brightness, režim okraje Clamp/Mirror. * Preview na zmenšeném ROI (např. 256×256) – bez zásahu do zdrojové vrstvy. * Akce Apply – provede filtr nad ROI (nebo celou vrstvou, není-li výběr). * Volitelné: 5×5 režim a předvolby (Box blur, Gaussian approx, Sharpen, Edge/Sobel/Prewitt).

3. Struktura pluginu

Struktura:

pykrita/
  convolution_lab.desktop
  convolution_lab/
    __init__.py
    convolution_lab.py
    presets.json    # volitelné (předvolby jader)

convolution_lab.desktop (zkráceně):

[Desktop Entry]
Type=Service
ServiceTypes=Krita/PythonPlugin
Name=ConvolutionLab
Comment=Universal 3x3/5x5 convolution with preview
X-Krita-Enabled=true
X-Krita-Python-Module=convolution_lab
X-Krita-Plugin-Category=Tools

4. UI (PyQt) — doporučené prvky

  • QTableWidget 3×3 (volitelně přepínač 5×5).
  • QCheckBox Normalize sum – vydělí hodnoty součtem (pokud ≠ 0).
  • QCheckBox Preserve brightness – pokud je součet ≈ 1 (po normalizaci), zachová jas; u edge jader (součet 0) použijte posun/clip.
  • QComboBox Borders: Clamp / Mirror.
  • QPushButton Preview – render do dočasné kopie ROI (ne do zdroje!).
  • QPushButton Apply – aplikuje do vrstvy.
  • QPushButton Presets… – naplní tabulku výběrem (Box/Gauss/Sharpen/Edge).

5. Konvoluce — implementační kostra

5.1. 4.1 Utility: aktivní dokument, ROI z výběru

from krita import Krita

def active_doc_node_roi():
    app = Krita.instance()
    doc = app.activeDocument()
    if not doc:
        return None, None, (0,0,0,0)
    node = doc.activeNode()
    sel = doc.selection()
    if sel:
        x,y,w,h = sel.x(), sel.y(), sel.width(), sel.height()
    else:
        x,y,w,h = 0, 0, doc.width(), doc.height()
    return doc, node, (x,y,w,h)

5.2. 4.2 Okrajové režimy (Clamp/Mirror)

def clamp(v, lo, hi):
    return lo if v < lo else hi if v > hi else v

def mirror(i, lo, hi):
    # zrcadlení do [lo,hi] včetně krajů
    span = hi - lo
    if span <= 0: return lo
    t = (i - lo) % (2*span)
    if t > span:
        t = 2*span - t
    return lo + t

5.3. 4.3 Vlastní konvoluční průchod (BGRA, jen B,G,R)

def convolve_bgra_block(src_bytes, w, h, kernel, ksize=3, border='clamp'):
    """
    src_bytes: bytes/bytearray BGRA (w*h*4)
    vrací nový bytearray BGRA
    """
    assert ksize in (3,5)
    dst = bytearray(len(src_bytes))
    src = memoryview(src_bytes)
    out = memoryview(dst)

    half = ksize // 2
    # předpočítej součet jádra a normalizaci
    ksum = sum(kernel)
    norm = 1.0
    if abs(ksum) > 1e-9:
        norm = 1.0 / ksum  # Normalize sum
    # vyber funkci okraje
    edgef = clamp if border == 'clamp' else mirror

    # hlavní smyčka po řádcích
    for y in range(h):
        for x in range(w):
            accB = accG = accR = 0.0
            for ky in range(ksize):
                sy = y + ky - half
                sy = edgef(sy, 0, h-1)
                for kx in range(ksize):
                    sx = x + kx - half
                    sx = edgef(sx, 0, w-1)
                    k = kernel[ky*ksize + kx]
                    idx = (sy*w + sx) * 4
                    b = src[idx + 0]
                    g = src[idx + 1]
                    r = src[idx + 2]
                    accB += k * b
                    accG += k * g
                    accR += k * r
            # normalizace
            accB *= norm; accG *= norm; accR *= norm
            # clamp to 0..255
            B = 0 if accB < 0 else 255 if accB > 255 else int(accB + 0.5)
            G = 0 if accG < 0 else 255 if accG > 255 else int(accG + 0.5)
            R = 0 if accR < 0 else 255 if accR > 255 else int(accR + 0.5)

            di = (y*w + x)*4
            out[di + 0] = B
            out[di + 1] = G
            out[di + 2] = R
            out[di + 3] = src[di + 3]  # alfa kopie 1:1
    return dst
Důležité:

Preserve brightness: u „rozostřovacích“ jader má smysl normalizovat součet na 1. U edge jader je součet často 0 – normalizace součtem nedává smysl. Řešení: pokud ksum==0, neškálujte součtem a jen clipujte do 0..255 (nebo přidejte globální gain).

5.4. 4.4 Aplikace do vrstvy (ROI + refresh)

def apply_kernel_to_active_layer(kernel, ksize=3, border='clamp'):
    doc, node, (x,y,w,h) = active_doc_node_roi()
    if not node or w==0 or h==0: return
    src = bytearray(node.pixelData(x, y, w, h))
    dst = convolve_bgra_block(src, w, h, kernel, ksize, border)
    node.setPixelData(bytes(dst), x, y, w, h)
    doc.refreshProjection()

6. Předvolby (doporučené matice)

  • Box blur 3×3: [[1,1,1],[1,1,1],[1,1,1]] Normalize sum: ON
  • Gaussian approx 3×3: [[1,2,1],[2,4,2],[1,2,1]] Normalize sum: ON
  • Sharpen 3×3: [[0,-1,0],[-1,5,-1],[0,-1,0]] Normalize sum: OFF (součet 1; brightness OK)
  • Edge (Laplacian): [[0,1,0],[1,-4,1],[0,1,0]] nebo [[1,1,1],[1,-8,1],[1,1,1]] Normalize sum: OFF (součet 0)
  • Prewitt/Sobel (na hrany X/Y) – volitelně jako dvojice jader

7. Preview na ROI (doporučený postup)

  1. Získejte ROI (výběr nebo celé plátno) a zmenšete jej (např. nearest/box) na max 256 px delší strana.
  2. Spočítejte konvoluci nad zmenšeným obrazem.
  3. Výsledek zobrazte v QLabel (pixmapa) v dialogu.
  4. Nezapisujte do zdroje, dokud uživatel nestiskne Apply.

8. Časování & výkon (tipy)

  • Řádkový průchod + memoryview (viz výše) místo „pixel po pixelu“ s indexováním listů – citelně rychlejší.
  • U 5×5 je vhodné vynechat výpočty pro A kanál (kopírovat).
  • ThreadPoolExecutor (po řádcích/blocích) může pomoci jen omezeně (GIL); prioritu dejte čistému Pythonu a menšímu overheadu.
  • Pro velmi velká plátna nabídněte progress a možnost Cancel.

9. Odevzdání (co má být v repu)

  • ConvolutionLab (adresář pluginu) – .desktop, init.py, convolution_lab.py.
  • README: instalace, kde v menu, popis parametrů, známá omezení.
  • Předvolby (JSON) – volitelné.
  • Ukázky: 3–4 PNG „před/po“ (blur, sharpen, edge).

10. Checklist kvality

  • Plugin se načte, akce je v Tools/Scripts.
  • Matice 3×3 editovatelná, Normalize sum funguje.
  • Clamp/Mirror okrajové chování bez artefaktů.
  • Preview je rychlý a nepíše do zdroje.
  • Apply zpracuje jen ROI dle výběru (jinak celé).
  • Zpracovává se jen B,G,R, A se kopíruje 1:1.
  • refreshProjection() po zápisu.

11. Hodnocení (rubrika 0–4)

  • Funkčnost (0–4): správné výsledky, okraje, normalizace, preview+apply.
  • Výkon (0–4): plynulé preview, rozumný čas na 2–4k, UI nepadá.
  • Kód (0–4): čitelný, rozdělený na UI/logic, komentáře u těžších částí.
  • UX (0–4): jasné popisky, bezpečné defaulty, předvolby (aspoň 3).

12. Rozšíření (bonus)

  • 5×5 režim (přepínač) a odpovídající jádra (Gauss 5×5).
  • Separable mód pro Gauss (X→Y) — příprava na Cvičení 3 (rychlost).
  • Absolute/Gain pro edge detekci.
  • Apply on copy – vytvořit duplikát vrstvy a filtr aplikovat na kopii (nedestruktivně).

13. Troubleshooting

  • Tmavý/světlý výsledek po blur: chybějící Normalize sum (musí být 1).
  • Zubaté okraje: špatné okrajové vzorkování – ověřte Clamp/Mirror.
  • Pomalé/freeze: vypněte preview na plné velikosti; dělejte downsample.
  • Nic se nezmění: zapisujete do jiné vrstvy, nebo chybí refreshProjection().
  • Rozpad alfa: upravujete A kanál – neměňte alfa, kopírujte ji.

14. Příloha — ukázka napojení UI na výpočet (zkráceno)

class ConvolutionDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        # ... vytvořte table 3x3, checky a combobox ...
        # self.table, self.chk_norm, self.chk_preserve, self.cmb_border
        # self.btn_preview, self.btn_apply

    def read_kernel(self):
        k = []
        for r in range(3):
            for c in range(3):
                try:
                    k.append(float(self.table.item(r,c).text()))
                except:
                    k.append(0.0)
        return k

def run_convolution(self):
    dlg = ConvolutionDialog()
    if dlg.exec_() != QDialog.Accepted:
        return
    kernel = dlg.read_kernel()
    border = 'mirror' if dlg.cmb_border.currentText().lower().startswith('mir') else 'clamp'
    apply_kernel_to_active_layer(kernel, 3, border)

PGA — Krita Python Plugins: Cvičení 3 (Konvoluce II – rychlost)

Autor: tým PGA :toc: macro

ZIP :toclevels: 3 :sectnums: :icons: font :source-highlighter: rouge :experimental: :lang: cs

Cíl cvičení: navázat na Cvičení 2 a urychlit filtry pomocí separabilního Gaussova rozostření (1D X→Y), implementovat Sobel (Gx, Gy) včetně magnitude (a volitelně orientace), a udělat benchmark (2k/4k, ROI). Stále platí: BGRA, zpracovat jen B,G,R, A kopírovat.

1. Předpoklady

  • Zvládáte plugin z Cvičení 1 (akce, ROI, BGRA, refresh).
  • Máte kostru z Cvičení 2 (konvoluce 3×3, okraje clamp/mirror, preview).
  • Nepočítáme s NumPy/Scipy (čistý Python).
Varování:

Windows Krita používá vlastní Python → import NumPy nejspíš selže (plugin se tiše nenačte). Cvičení proto řešte čistým Pythonem.

2. Zadání

Vytvořte plugin FastFilters: * Panel Separable Gaussian: sigma, radius (nebo auto radius = ceil(3σ)), okraj Clamp/Mirror, Preview + Apply. * Panel Sobel: volba výstupu: Gx, Gy, Magnitude, (volitelně Orientation v ° nebo barevná vizualizace), Preview + Apply. * Panel Benchmark: změří časy pro 2–3 rozlišení / ROI; vypíše tabulku (CSV do schránky nebo do logu). * Stále BGRA, A kopírujte 1:1, vše selection-safe (ROI).

3. Separable Gauss — teorie do praxe

Gaussovo jádro je separabilní → 2 průchody 1D: nejprve horizontálně (řádky), pak vertikálně (sloupce). Tím snížíte složitost z O(k² * N) na O(2k * N).

3.1. 2.1 Generátor 1D jádra

import math

def gaussian_kernel_1d(sigma, radius=None):
    if sigma <= 0: return [1.0]
    if radius is None:
        radius = int(math.ceil(3.0 * sigma))  # ~99.7 % hmoty
    w = 2*radius + 1
    k = [0.0]*w
    s2 = 2.0 * sigma * sigma
    acc = 0.0
    for i in range(w):
        x = i - radius
        v = math.exp(-(x*x)/s2)
        k[i] = v
        acc += v
    # normalizace na 1.0
    for i in range(w):
        k[i] /= acc
    return k, radius

3.2. 2.2 Okraje (re-use z Cvičení 2)

Použijte stejné clamp/mirror funkce. U separabilního průchodu budete sahat na indexy (x±r), (y±r).

3.3. 2.3 1D konvoluce přes řádky a sloupce

def convolve_rows_bgr(src_bytes, w, h, kernel, radius, border='clamp'):
    src = memoryview(src_bytes)
    dst = bytearray(len(src_bytes))
    out = memoryview(dst)
    edgef = clamp if border == 'clamp' else mirror

    for y in range(h):
        row_off = y * w * 4
        for x in range(w):
            accB = accG = accR = 0.0
            for k in range(-radius, radius+1):
                sx = edgef(x + k, 0, w-1)
                i = row_off + sx*4
                # BGRA
                b = src[i+0]; g = src[i+1]; r = src[i+2]
                wgt = kernel[k + radius]
                accB += wgt * b
                accG += wgt * g
                accR += wgt * r
            di = row_off + x*4
            out[di+0] = 0 if accB<0 else 255 if accB>255 else int(accB+0.5)
            out[di+1] = 0 if accG<0 else 255 if accG>255 else int(accG+0.5)
            out[di+2] = 0 if accR<0 else 255 if accR>255 else int(accR+0.5)
            out[di+3] = src[di+3]  # alfa
    return dst

def convolve_cols_bgr(src_bytes, w, h, kernel, radius, border='clamp'):
    src = memoryview(src_bytes)
    dst = bytearray(len(src_bytes))
    out = memoryview(dst)
    edgef = clamp if border == 'clamp' else mirror

    for y in range(h):
        for x in range(w):
            accB = accG = accR = 0.0
            for k in range(-radius, radius+1):
                sy = edgef(y + k, 0, h-1)
                i = (sy*w + x)*4
                b = src[i+0]; g = src[i+1]; r = src[i+2]
                wgt = kernel[k + radius]
                accB += wgt * b
                accG += wgt * g
                accR += wgt * r
            di = (y*w + x)*4
            out[di+0] = 0 if accB<0 else 255 if accB>255 else int(accB+0.5)
            out[di+1] = 0 if accG<0 else 255 if accG>255 else int(accG+0.5)
            out[di+2] = 0 if accR<0 else 255 if accR>255 else int(accR+0.5)
            out[di+3] = src[di+3]
    return dst

def gaussian_blur_separable(src_bytes, w, h, sigma, radius=None, border='clamp'):
    kernel, radius = gaussian_kernel_1d(sigma, radius)
    tmp = convolve_rows_bgr(src_bytes, w, h, kernel, radius, border)
    out = convolve_cols_bgr(tmp, w, h, kernel, radius, border)
    return out

3.4. 2.4 Aplikace do vrstvy (ROI + refresh)

def apply_gaussian_to_active_layer(sigma, radius=None, border='clamp'):
    doc, node, (x,y,w,h) = active_doc_node_roi()
    if not node or w==0 or h==0: return
    src = bytearray(node.pixelData(x, y, w, h))
    dst = gaussian_blur_separable(src, w, h, sigma, radius, border)
    node.setPixelData(bytes(dst), x, y, w, h)
    doc.refreshProjection()

4. Sobel (Gx, Gy, Magnitude, Orientation)

Sobel funguje jako dvojice 3×3 jader. Počítejte gradienty pro každý z RGB kanálů (nebo na luminanci – rychlejší/čistší), a pak z toho odvozujte magnitudu (sílu hrany).

4.1. 3.1 Jádra

SOBEL_X = [-1,0,1, -2,0,2, -1,0,1]
SOBEL_Y = [-1,-2,-1, 0,0,0, 1,2,1]

4.2. 3.2 Výpočet Gx/Gy a magnitude (na luminanci Y – doporučeno)

def rgb_to_luma(r,g,b):
    # Rec.709
    return 0.2126*r + 0.7152*g + 0.0722*b

def sobel_luma_bgra(src_bytes, w, h, border='clamp', want='magnitude'):
    src = memoryview(src_bytes)
    out = bytearray(len(src_bytes))
    dst = memoryview(out)
    edgef = clamp if border == 'clamp' else mirror
    kx = SOBEL_X; ky = SOBEL_Y

    for y in range(h):
        for x in range(w):
            gx = gy = 0.0
            # 3x3 okno
            idxk = 0
            for ky_off in (-1,0,1):
                sy = edgef(y+ky_off, 0, h-1)
                for kx_off in (-1,0,1):
                    sx = edgef(x+kx_off, 0, w-1)
                    i = (sy*w + sx)*4
                    b,g,r = src[i+0], src[i+1], src[i+2]
                    yv = rgb_to_luma(r,g,b)
                    gx += kx[idxk] * yv
                    gy += ky[idxk] * yv
                    idxk += 1
            mag = (gx*gx + gy*gy) ** 0.5
            # normalizace jednoduchým škálováním
            m = int(max(0, min(255, mag)))
            di = (y*w + x)*4
            if want == 'gx':
                v = int(max(0, min(255, gx + 128)))  # posun kvůli vizualizaci
                dst[di+0]=dst[di+1]=dst[di+2]=v
            elif want == 'gy':
                v = int(max(0, min(255, gy + 128)))
                dst[di+0]=dst[di+1]=dst[di+2]=v
            elif want == 'orientation':
                # orientace v rozsahu 0..180° → mapujte do 0..255
                import math
                ang = math.degrees(math.atan2(gy, gx))  # -180..180
                ang = abs(ang)                           # 0..180
                v = int(ang * (255.0/180.0))
                dst[di+0]=dst[di+1]=dst[di+2]=v
            else:  # magnitude (default)
                dst[di+0]=dst[di+1]=dst[di+2]=m
            dst[di+3] = src[di+3]  # alfa
    return out

4.3. 3.3 Aplikace Sobelu

def apply_sobel_to_active_layer(mode='magnitude', border='clamp'):
    doc, node, (x,y,w,h) = active_doc_node_roi()
    if not node or w==0 or h==0: return
    src = bytearray(node.pixelData(x, y, w, h))
    dst = sobel_luma_bgra(src, w, h, border, want=mode)
    node.setPixelData(bytes(dst), x, y, w, h)
    doc.refreshProjection()

5. Preview & UI

  • Preview vždy spočítejte na downsample kopii ROI (delší strana max ~256 px).
  • U Gauss σ udělejte slider + číslo; u Sobel přepínače gx/gy/magnitude/orientation.
  • Border: stejný Clamp/Mirror jako v Cvičení 2.

6. Benchmark panel

Změřte čas jednotlivých kroků a vypište tabulku (log/CSV).

import time

def bench_filter(func, label, x,y,w,h, repeats=1):
    t0 = time.perf_counter()
    for _ in range(repeats):
        func()
    t1 = time.perf_counter()
    dt = (t1 - t0) / repeats
    print(f"{label}; {w}x{h}; {dt*1000:.1f} ms")
    return dt

Příklad běhu: * Gauss σ=1.6 (separable) – ROI 1920×1080. * Gauss σ=3.0 (separable) – ROI 3840×2160. * Sobel magnitude – ROI 1920×1080. Zapište do README krátkou tabulku (rozlišení, čas, border).

7. Odevzdání

  • Adresář FastFilters (desktop, init.py, fast_filters.py).
  • README: jak spustit, parametry, tabulka benchmarku (alespoň 2 rozlišení), známá omezení.
  • Ukázky: PNG „před/po“ (Gauss, Sobel magnitude).

8. Checklist kvality

  • Gauss je separabilní (dva průchody 1D), normalizovaný kernel.
  • Okraje Clamp/Mirror korektní.
  • Sobel: gx/gy/magnitude (volitelně orientation) funguje.
  • Preview je rychlý a nezasahuje do zdroje.
  • Apply respektuje ROI dle výběru.
  • A kopie 1:1 (neměnit).
  • Benchmark – srozumitelné výsledky v README.
  • refreshProjection() po zápisu.

9. Hodnocení (rubrika 0–4)

  • Funkčnost (0–4): správné výsledky Gauss/Sobel, preview+apply, okraje.
  • Výkon (0–4): separabilita přináší výrazné zrychlení; benchmark doručen.
  • Kód (0–4): čisté oddělení UI/logic, komentáře u klíčových částí, opakované využití utilit.
  • UX (0–4): přehledné ovládání, bezpečné defaulty, rychlý preview.

10. Rozšíření (bonus)

  • Unsharp mask: out = original + amount * (original - gaussian_blur).
  • Adaptive σ: různá σ pro X/Y (anisotropic blur – stále separabilní).
  • Sobel na barvu: magnitude jako max/avg přes R,G,B (namísto luminance).
  • Autoscale magnitude: najdi lokální maximum a škáluj mapu do 0..255.

11. Troubleshooting

  • „Gauss tmaví/světlá“ – jádro není normalizované (součet ≠ 1).
  • Artefakty na hranách – zkontrolujte správné clamp/mirror u indexů.
  • Pomalý preview – nedělejte ho na plnou velikost; downsample.
  • Změněná průhlednost – omylem upravujete A; A vždy kopírujte.
  • UI zamrzá – pro dlouhé operace použijte QProgressDialog + QApplication.processEvents().

PGA — Krita Python Plugins: Cvičení 4 (Frekvenční filtr – FFT/DFT)

Autor: tým PGA :toc: macro

ZIP :toclevels: 3 :sectnums: :icons: font :source-highlighter: rouge :experimental: :lang: cs

Cíl cvičení: napsat plugin FreqLab pro náhled spektra (log-magnitude) vybrané oblasti (ROI) a aplikovat low-pass / high-pass / band-pass filtr. Primárně počítejte přes NumPy FFT (pokud je dostupné), jinak použijte fallback DFT na malém náhledu (např. 64×64). Zpracování dělejte selection-safe, respektujte BGRA (pracujte na luminanci nebo po kanálech) a nabídněte Preview vs. Apply.

1. Předpoklady

  • Umíte pracovat s ROI (dle výběru) a číst/zapisovat pixely (BGRA) – viz Cvičení 1–3.
  • Znáte základní PyQt dialogy (slidery, combobox, tlačítka).
  • Víte, že na Windows má Krita vlastní Pythonnumpy nemusí být k dispozici.
Varování:

Pokud import numpy selže, plugin se může tiše nenačíst. Použijte try/except a přepněte se do fallback režimu s malou DFT jen pro náhled (např. 64×64), zatímco ostré Apply uděláte v prostorové doméně (např. pomocí Gauss blur + unsharp ap.), nebo necháte Apply fungovat jen, když je NumPy dostupné. Uveďte to zřetelně v UI.

2. Zadání

Vytvořte plugin FreqLab: * zobrazí log-magnitude spektrum aktuálního ROI, * umožní vybrat filtr: Low-pass, High-pass, Band-pass (kruhové masky ve frekvenční doméně), * parametry: radius (LP/HP), inner/outer radius (BP), volba okna pro potlačení zvonění (None/Hann), * přepínač Process: Luma only / Per-channel (RGB), * Preview (rychlý náhled) a Apply (aplikace do vrstvy nebo na kopii vrstvy), * bezpečný režim: pokud není NumPy, UI to oznámí a Preview běží na 64×64 (DFT, jen luma), Apply: (a) nedostupné, nebo (b) alternativní aproximace (např. Gauss blur versus unsharp).

3. Teorie – stručně pro praxi

  • FFT (rychlá Fourierova transformace) převádí obraz do frekvenční domény: nízké frekvence ~ hladké změny, vysoké ~ hrany/detail.
  • Pro vizualizaci magnitude se používá log (např. log(1+|F|)) a fftshift (DC doprostřed).
  • Kruhové masky:
    • Low-pass: nechá jen frekvence s r ≤ R.
    • High-pass: nechá r ≥ R.
    • Band-pass: nechá R_in ≤ r ≤ R_out.
  • Okno (Hann/Hamming) v prostoru před FFT zmírní zvonění (ringing) způsobené ostrým ořezem.
  • Po filtraci: iFFT → reálná část → normalizace/clamp → zápis do BGRA.

4. UI návrh (PyQt)

  • QComboBox Filter: Low-pass, High-pass, Band-pass.
  • Radius (pro LP/HP), Inner/Outer Radius (pro BP) – v pixelech frekvenční mřížky (relativní k menšímu rozměru FFT).
  • Window: None / Hann.
  • Process: Luma only / Per-channel (RGB).
  • Tlačítka: Preview, Apply, To New Layer (volitelně), Close.
  • Panel Status/Hint: dostupnost NumPy, velikost ROI, fallback mód.

5. Implementační kostry

5.1. 4.1 Bezpečný import NumPy

def try_import_numpy():
    try:
        import numpy as np
        return np
    except Exception:
        return None

NP = try_import_numpy()
HAS_NUMPY = NP is not None

5.2. 4.2 Získání ROI + převod na lumu

from krita import Krita

def get_doc_node_roi():
    app = Krita.instance()
    doc = app.activeDocument()
    if not doc:
        return None, None, (0,0,0,0)
    node = doc.activeNode()
    sel = doc.selection()
    if sel:
        x,y,w,h = sel.x(), sel.y(), sel.width(), sel.height()
    else:
        x,y,w,h = 0,0,doc.width(), doc.height()
    return doc, node, (x,y,w,h)

def bgra_to_luma_bytes(bgra, w, h):
    # Rec.709 luma
    out = bytearray(w*h)
    mv = memoryview(bgra)
    for i in range(0, len(mv), 4):
        b = mv[i+0]; g = mv[i+1]; r = mv[i+2]
        y = 0.2126*r + 0.7152*g + 0.0722*b
        out[i//4] = 0 if y<0 else 255 if y>255 else int(y+0.5)
    return out

5.3. 4.3 FFT náhled (NumPy varianta)

def fft_preview_numpy(gray_u8, w, h, window='none'):
    import numpy as np
    g = np.frombuffer(gray_u8, dtype=np.uint8).astype(np.float32).reshape(h, w)
    # okno (proti ringing)
    if window.lower().startswith('hann'):
        wy = np.hanning(h); wx = np.hanning(w)
        g = g * wy[:,None] * wx[None,:]
    # FFT
    F = np.fft.fft2(g)
    Fshift = np.fft.fftshift(F)
    mag = np.log1p(np.abs(Fshift))
    mag /= mag.max() if mag.max() > 1e-9 else 1.0
    return (mag * 255.0).astype(np.uint8)

5.4. 4.4 Kruhové masky ve frekvenční doméně

def make_circular_mask(h, w, mode, r_in_px=0, r_out_px=10):
    import numpy as np
    cy, cx = h//2, w//2
    Y, X = np.ogrid[:h, :w]
    R = np.sqrt((Y-cy)**2 + (X-cx)**2)
    M = np.zeros((h,w), dtype=np.float32)
    if mode == 'lowpass':
        M[R <= r_out_px] = 1.0
    elif mode == 'highpass':
        M[R >= r_out_px] = 1.0
    else: # bandpass
        M[(R >= r_in_px) & (R <= r_out_px)] = 1.0
    return M

5.5. 4.5 Aplikace filtru (NumPy) – luma only

def apply_fft_filter_numpy(gray_u8, w, h, mode, r1, r2, window='none'):
    import numpy as np
    g = np.frombuffer(gray_u8, dtype=np.uint8).astype(np.float32).reshape(h, w)
    if window.lower().startswith('hann'):
        wy = np.hanning(h); wx = np.hanning(w)
        g = g * wy[:,None] * wx[None,:]
    F  = np.fft.fft2(g)
    Fs = np.fft.fftshift(F)
    if mode == 'bandpass':
        M = make_circular_mask(h, w, 'bandpass', r_in_px=r1, r_out_px=r2)
    elif mode == 'lowpass':
        M = make_circular_mask(h, w, 'lowpass', r_out_px=r2)
    else:
        M = make_circular_mask(h, w, 'highpass', r_out_px=r2)
    Fs_filtered = Fs * M
    Fi = np.fft.ifftshift(Fs_filtered)
    out = np.fft.ifft2(Fi).real
    # normalizace do 0..255
    mn, mx = float(out.min()), float(out.max())
    if mx - mn < 1e-9:
        out[:] = 0
    else:
        out = (out - mn) * (255.0/(mx-mn))
    return out.astype(np.uint8)

5.6. 4.6 Zápis do vrstvy (BGRA) – luma merge

def merge_luma_into_bgra(bgra, luma_u8, w, h):
    # nahradí luminanci; jednoduchá varianta: přemapuje RGB do stejné Y
    mv = memoryview(bgra)
    out = bytearray(len(mv))
    for i in range(0, len(mv), 4):
        Y = luma_u8[i//4]
        # ponecháme barvu přibližně via scale ke stejné světlosti:
        # jednoduché: všem RGB dáme Y (grayscale look)
        out[i+0] = Y
        out[i+1] = Y
        out[i+2] = Y
        out[i+3] = mv[i+3]  # alfa
    return out

5.7. 4.7 Fallback DFT (bez NumPy) – jen pro malý náhled (např. 64×64)

def tiny_dft_2d(gray_u8, n):
    # gray_u8: n*n uint8
    # návrat: komplexní 2D pole (separovaně re/im v Pythonu)
    import math
    g = [float(gray_u8[i]) for i in range(n*n)]
    F_re = [0.0]*(n*n); F_im = [0.0]*(n*n)
    two_pi = 2.0*math.pi
    for v in range(n):
        for u in range(n):
            sum_re = 0.0; sum_im = 0.0
            for y in range(n):
                for x in range(n):
                    idx = y*n + x
                    ang = two_pi*((u*x + v*y)/n)
                    val = g[idx]
                    sum_re += val*math.cos(-ang)
                    sum_im += val*math.sin(-ang)
            F_re[v*n + u] = sum_re
            F_im[v*n + u] = sum_im
    return F_re, F_im

def dft_log_magnitude(F_re, F_im, n):
    import math
    mag = [0.0]*(n*n)
    for i in range(n*n):
        mag[i] = math.log1p((F_re[i]**2 + F_im[i]**2)**0.5)
    mx = max(mag) if mag else 1.0
    out = bytearray(n*n)
    for i,m in enumerate(mag):
        out[i] = int(255.0*(m/mx)) if mx>1e-9 else 0
    return out
Upozornění:

DFT je kvadraticky pomalé. Používejte jen pro náhled do cca 64×64. Ostré Apply v DFT fallback režimu raději zakažte nebo nabídněte alternativu v prostorové doméně.

6. Preview workflow

  1. Získejte ROI (výběr → bbox; jinak celé plátno).
  2. Downsamplujte (např. max_side = 256 s NumPy, 64 bez NumPy).
  3. Vypočítejte spektrum a zobrazte log-magnitude v QLabel (pixmapa).
  4. Při změně parametrů (radius/typ filtru) přepočítejte preview.

7. Apply workflow

  • Luma only: spočítejte lumu (plná velikost), FFT→mask→iFFT→normalizace → napište zpět jako grayscale RGB (A kopie).
  • Per-channel: rozdělit BGRA na B,G,R → FFT per-kanál → maska → iFFT → složit zpět.
  • Volitelné: Apply to new layer (duplikujte vrstvu a efekt proveďte na kopii).
  • Po zápisu: doc.refreshProjection(); při vizuálním problému krátce přepněte viditelnost vrstvy.

8. Odevzdání

  • Adresář pluginu FreqLab (.desktop, init.py, freq_lab.py).
  • README: dostupnost NumPy, fallback chování, limity, postup instalace.
  • GIF/PNG ukázky: originál, log-magnitude náhled, LP/HP/BP výsledek.
  • Volitelně: preset JSON s posledními parametry (QSettings/JSON).

9. Checklist kvality

  • UI jasně ukazuje, zda běží NumPy nebo fallback.
  • Preview rychle reaguje (downsample; 256/64).
  • Apply funguje (luma only; per-channel volitelně).
  • A kanál zachován 1:1, zápis pouze do B,G,R.
  • refreshProjection() po zápisu; bez pádů na velkých ROI.
  • Dokumentace limitů (ringing, okno, bez NumPy).

10. Hodnocení (rubrika 0–4)

  • Funkčnost (0–4): náhled spektra, LP/HP/BP filtr, Apply (alespoň luma).
  • UX (0–4): srozumitelné ovládání, status/dostupnost NumPy, rychlé preview.
  • Technika (0–4): správná normalizace, fftshift/log, ošetření okna.
  • Kód (0–4): oddělené části (I/O, FFT, UI), fallback bez NumPy.

11. Rozšíření (bonus)

  • Band-stop (notch) s možností vyříznout úzké pásmo.
  • Volná maska: kreslení masky ve spektru (myší) → aplikace.
  • Hybridní Apply v DFT fallbacku: přepočítat po dlaždicích (tiles) 128×128 pomocí FFT, pokud se podaří dynamicky načíst NumPy (např. přes krita-pip).

12. Troubleshooting

  • „Plugin se nenačetl“ – patrně selhal import numpy. Obalte import try/except a ukažte fallback status.
  • „Černé/špatné spektrum“ – chybí log1p nebo normalizace; zkontrolujte fftshift.
  • „Zvonění po filtraci“ – zapněte Hann window nebo použijte hladší filtr (Gauss v frekvenční doméně).
  • „Barvy divné po Apply (per-channel)“ – pozor na různé rozsahy/normalizaci u kanálů; sjednoťte škálu.
  • „UI zamrzá“ – náhled držte malý; při Apply ukažte QProgressDialog a processEvents().

13. Příloha — mini-skeleton pluginu (zkráceno)

# freq_lab.py
from krita import Extension
from PyQt5.QtWidgets import QDialog, QLabel, QPushButton, QComboBox, QSpinBox, QVBoxLayout, QHBoxLayout

class FreqLab(Extension):
    def __init__(self, parent):
        super().__init__(parent)
        self.app = parent

    def setup(self): pass

    def createActions(self, window):
        act = window.createAction("pga_freq_lab", "FreqLab (FFT/DFT)", "tools/scripts")
        act.triggered.connect(self.show_dialog)

    def show_dialog(self):
        dlg = FreqDialog(self.app)
        dlg.exec_()

class FreqDialog(QDialog):
    def __init__(self, app, parent=None):
        super().__init__(parent)
        self.app = app
        self.setWindowTitle("FreqLab — FFT/DFT")
        self.cmb_filter = QComboBox(); self.cmb_filter.addItems(["Low-pass","High-pass","Band-pass"])
        self.sp_r1 = QSpinBox(); self.sp_r1.setRange(1, 2048); self.sp_r1.setValue(8)
        self.sp_r2 = QSpinBox(); self.sp_r2.setRange(1, 4096); self.sp_r2.setValue(24)
        self.cmb_window = QComboBox(); self.cmb_window.addItems(["None","Hann"])
        self.btn_preview = QPushButton("Preview")
        self.btn_apply   = QPushButton("Apply")
        self.lbl_img = QLabel("Spectrum preview here")
        lay = QVBoxLayout(self)
        row = QHBoxLayout(); row.addWidget(self.cmb_filter); row.addWidget(self.sp_r1); row.addWidget(self.sp_r2); lay.addLayout(row)
        row2= QHBoxLayout(); row2.addWidget(self.cmb_window); row2.addWidget(self.btn_preview); row2.addWidget(self.btn_apply); lay.addLayout(row2)
        lay.addWidget(self.lbl_img)
        self.btn_preview.clicked.connect(self.on_preview)
        self.btn_apply.clicked.connect(self.on_apply)

    def on_preview(self):
        # 1) get ROI + downsample; 2) compute spectrum (NumPy or DFT fallback); 3) set pixmap to lbl_img
        pass

    def on_apply(self):
        # if NumPy: compute filtered image on full ROI and write back; else: warn or use spatial-domain fallback
        pass

PGA — Krita Python Plugins: Cvičení 5 (ColorLab — Sepia, Grayscale, Curves, HSL, Gradient Map)

Autor: tým PGA :toc: macro

ZIP :toclevels: 3 :sectnums: :icons: font :source-highlighter: rouge :experimental: :lang: cs

Cíl cvičení: vytvořit plugin ColorLab, který bezpečně a rychle provádí barevné transformace nad ROI: grayscale (více metod), sepia (3×3 matice), HSL/HSV úpravy, Curves/Levels (přes LUT), a Gradient Map (toning). Důraz na BGRA pořadí, A beze změny, preview na zmenšeném ROI a Apply na plnou oblast.

1. Předpoklady

  • Zvládáte skeleton pluginu (Cvičení 1) a práci s ROI + pixelData()/setPixelData() (Cvičení 1–3).
  • Umíte postavit jednoduché PyQt dialogy a číst hodnoty ze sliderů/comboboxů.
  • Víte, že na Windows má Krita vlastní Python (bez NumPy) → vše řešíme čistým Pythonem.

2. Zadání

Vytvořte ColorLab s těmito bloky: * Grayscale — režimy: Average, Luma 601, Luma 709 (doporučeno), Desaturate (min/max průměr). * Sepia — 3×3 barevná matice (viz níže), intenzita (mix původní ↔ sepia). * Curves/Levels — Master křivka (0–255) + volitelně R/G/B; implementace přes LUT (256 položek). * HSL/HSVHue shift (–180..+180°), Saturation (0..200 %), Lightness/Value (±). * Gradient Map — mapování luminance → barva (2–5 uzlů s barvou a pozicí 0..1). * Preview (na zmenšeném ROI) a Apply (na plné ROI), přepínač Apply on copy.

3. UI návrh (PyQt)

  • Tabbed/accordion layout:
    • Grayscale: ComboBox s metodou + Strength (0..100 %, mix s původní barvou).
    • Sepia: Strength (0..100 %), Matrix preset (čitelné hodnoty).
    • Curves/Levels: pro zjednodušení v tomto cvičení 3 body (stíny, střed, světla) → z nich vygenerujte LUT 256; volitelně přepínač Master/R/G/B.
    • HSL/HSV: tři slidery (Hue Shift, Saturation, Lightness/Value), přepínač HSL/HSV.
    • Gradient Map: 2–5 uzlů (pozice 0..1, barva #RRGGBB), interpolace linear.
  • Tlačítka: Preview, Apply, Apply on copy, Reset, Close.
  • Status řádek: velikost ROI, downsample faktor, čas posledního preview.

4. Implementace — stavebnice

4.1. 3.1 ROI & čtení/zápis (BGRA)

def get_doc_node_roi(app):
    doc = app.activeDocument()
    if not doc: return None, None, (0,0,0,0)
    node = doc.activeNode()
    sel  = doc.selection()
    if sel:
        x,y,w,h = sel.x(), sel.y(), sel.width(), sel.height()
    else:
        x,y,w,h = 0, 0, doc.width(), doc.height()
    return doc, node, (x,y,w,h)

4.2. 3.2 Grayscale (metody)

Koeficienty luma: * Rec.601: Y = 0.299 R + 0.587 G + 0.114 B * Rec.709: Y = 0.2126 R + 0.7152 G + 0.0722 B (doporučeno) * Average: (R+G+B)/3 * Desaturate: (max(R,G,B)+min(R,G,B))/2

def gray_pixel(r,g,b, mode="709"):
    if mode == "avg":
        return (r+g+b)//3
    if mode == "desat":
        return (max(r,g,b) + min(r,g,b))//2
    if mode == "601":
        return int(0.299*r + 0.587*g + 0.114*b + 0.5)
    # default 709
    return int(0.2126*r + 0.7152*g + 0.0722*b + 0.5)

def apply_grayscale_bgra_block(bytes_in, w, h, mode="709", strength=1.0):
    data = bytearray(bytes_in)
    mv = memoryview(data)
    for i in range(0, len(mv), 4):
        b,g,r,a = mv[i+0], mv[i+1], mv[i+2], mv[i+3]
        y = gray_pixel(r,g,b, mode)
        if strength >= 0.999:
            mv[i+0] = mv[i+1] = mv[i+2] = y
        else:
            mv[i+0] = int((1-strength)*b + strength*y + 0.5)
            mv[i+1] = int((1-strength)*g + strength*y + 0.5)
            mv[i+2] = int((1-strength)*r + strength*y + 0.5)
        mv[i+3] = a
    return data

4.3. 3.3 Sepia (3×3 matice + mix)

Běžně používané sepia jádro:

0.3930.7690.189
0.3490.6860.168
0.2720.5340.131
SEPIA = (
    (0.393, 0.769, 0.189),
    (0.349, 0.686, 0.168),
    (0.272, 0.534, 0.131),
)

def mat3_apply(r,g,b, M):
    r2 = M[0][0]*r + M[0][1]*g + M[0][2]*b
    g2 = M[1][0]*r + M[1][1]*g + M[1][2]*b
    b2 = M[2][0]*r + M[2][1]*g + M[2][2]*b
    return int(min(255,max(0,r2))+0.5), int(min(255,max(0,g2))+0.5), int(min(255,max(0,b2))+0.5)

def apply_sepia_bgra_block(bytes_in, w, h, strength=1.0, M=SEPIA):
    data = bytearray(bytes_in); mv = memoryview(data)
    s = max(0.0, min(1.0, strength))
    for i in range(0, len(mv), 4):
        b,g,r,a = mv[i], mv[i+1], mv[i+2], mv[i+3]
        r2,g2,b2 = mat3_apply(r,g,b, M)
        if s >= 0.999:
            mv[i], mv[i+1], mv[i+2] = b2, g2, r2
        else:
            mv[i]   = int((1-s)*b + s*b2 + 0.5)
            mv[i+1] = int((1-s)*g + s*g2 + 0.5)
            mv[i+2] = int((1-s)*r + s*r2 + 0.5)
        mv[i+3] = a
    return data

4.4. 3.4 Curves/Levels → LUT 256

Zjednodušení: použijte 3 kontrolní body (in: 0, mid, 255) → linear interpolace do LUT.

def build_curve_lut(p0=(0,0), p1=(128,128), p2=(255,255)):
    # body jsou v rozsahu 0..255
    x0,y0 = p0; x1,y1 = p1; x2,y2 = p2
    lut = [0]*256
    for x in range(0, x1+1):
        t = (x - x0) / max(1, (x1 - x0))
        y = int((1-t)*y0 + t*y1 + 0.5)
        lut[x] = max(0, min(255, y))
    for x in range(x1+1, 256):
        t = (x - x1) / max(1, (x2 - x1))
        y = int((1-t)*y1 + t*y2 + 0.5)
        lut[x] = max(0, min(255, y))
    return lut

def apply_lut_bgra_block(bytes_in, w, h, lutR, lutG, lutB):
    data = bytearray(bytes_in); mv = memoryview(data)
    for i in range(0, len(mv), 4):
        b,g,r,a = mv[i], mv[i+1], mv[i+2], mv[i+3]
        mv[i]   = lutB[b]
        mv[i+1] = lutG[g]
        mv[i+2] = lutR[r]
        mv[i+3] = a
    return data

4.5. 3.5 HSL/HSV (Hue/Sat/Lightness or Value)

Pro přesnost preferujte HSL pro „Lightness“ a HSV pro „Value“. Níže je rychlá integer-friendly varianta (stačí na účely kurzu).

def rgb_to_hsv_i(r,g,b):
    mx = max(r,g,b); mn = min(r,g,b); diff = mx - mn
    v = mx
    s = 0 if mx == 0 else int(255 * diff / mx)
    if diff == 0: h = 0
    else:
        if mx == r: h = (60 * (g - b) // diff) % 360
        elif mx == g: h = (60 * (b - r) // diff) + 120
        else: h = (60 * (r - g) // diff) + 240
    return h, s, v

def hsv_to_rgb_i(h,s,v):
    if s == 0:
        return v, v, v
    s = s/255.0; v = v/255.0
    hi = int(h/60) % 6; f = (h/60) - int(h/60)
    p = v*(1-s); q = v*(1-s*f); t = v*(1-(1-f)*s)
    if hi==0: r,g,b = v,t,p
    elif hi==1: r,g,b = q,v,p
    elif hi==2: r,g,b = p,v,t
    elif hi==3: r,g,b = p,q,v
    elif hi==4: r,g,b = t,p,v
    else: r,g,b = v,p,q
    return int(r*255+0.5), int(g*255+0.5), int(b*255+0.5)

def apply_hsv_bgra_block(bytes_in, w, h, hue_shift_deg=0, sat_pct=100, val_pct=100):
    data = bytearray(bytes_in); mv = memoryview(data)
    for i in range(0, len(mv), 4):
        b,g,r,a = mv[i], mv[i+1], mv[i+2], mv[i+3]
        h,s,v = rgb_to_hsv_i(r,g,b)
        h = (h + hue_shift_deg) % 360
        s = int(s * (sat_pct/100.0)); s = 0 if s<0 else 255 if s>255 else s
        v = int(v * (val_pct/100.0)); v = 0 if v<0 else 255 if v>255 else v
        r2,g2,b2 = hsv_to_rgb_i(h,s,v)
        mv[i], mv[i+1], mv[i+2], mv[i+3] = b2, g2, r2, a
    return data

4.6. 3.6 Gradient Map (luminance → barva)

Držte 2–5 uzlů: pozice t∈[0,1], barva R,G,B (0..255); lineární interpolace.

def lerp(a,b,t): return int(a + (b-a)*t + 0.5)

def gradient_color(stops, t):
    # stops: list[(t, (r,g,b))], t v asc. pořadí
    if t <= stops[0][0]: return stops[0][1]
    if t >= stops[-1][0]: return stops[-1][1]
    for i in range(len(stops)-1):
        t0,c0 = stops[i]; t1,c1 = stops[i+1]
        if t0 <= t <= t1:
            u = (t - t0) / max(1e-9, (t1 - t0))
            r = lerp(c0[0], c1[0], u)
            g = lerp(c0[1], c1[1], u)
            b = lerp(c0[2], c1[2], u)
            return (r,g,b)
    return stops[-1][1]

def apply_gradient_map_bgra_block(bytes_in, w, h, stops, gray_mode="709"):
    data = bytearray(bytes_in); mv = memoryview(data)
    for i in range(0, len(mv), 4):
        b,g,r,a = mv[i], mv[i+1], mv[i+2], mv[i+3]
        y = gray_pixel(r,g,b, gray_mode) / 255.0
        r2,g2,b2 = gradient_color(stops, y)
        mv[i], mv[i+1], mv[i+2], mv[i+3] = b2, g2, r2, a
    return data

5. Preview & Apply workflow

  • Preview: získejte ROI → downsample (např. delší strana max 512 px) → aplikujte aktivní bloky (v doporučeném pořadí) → zobrazte v QLabel.
  • Apply: načtěte plné ROI → aplikujte aktivní bloky (stejná konfigurace) → setPixelData(…​)doc.refreshProjection().
  • Apply on copy: duplikujte node (nebo přidejte novou vrstvu) a efekt proveďte tam.

6. Odevzdání

  • Adresář ColorLab (.desktop, init.py, color_lab.py), případně grad_presets.json.
  • README: popis voleb, pořadí pipeline, limity (8bit přesnost, bez lineárního prostoru), screenshoty dialogu.
  • Ukázky PNG: grayscale metody, sepia (0/50/100 %), curves (S-shape), HSL (±saturace), Gradient Map (duotone).

7. Checklist kvality

  • BGRA pořadí, A beze změny.
  • Preview rychlé, nezasahuje do zdroje (downsample).
  • Apply respektuje ROI dle výběru; bez výběru celé plátno.
  • Curves/Levels přes LUT 256 (Master; volitelně R/G/B).
  • HSL/HSV funguje (Hue shift ±180°, sat/value %).
  • Sepia přes 3×3 matici + Strength (mix).
  • Gradient Map interpoluje 2–5 uzlů správně (linear).
  • refreshProjection() po zápisu, bez pádů.

8. Hodnocení (rubrika 0–4)

  • Funkčnost (0–4): správné výsledky všech bloků; mix/pořadí pipeline.
  • Výkon (0–4): plynulé preview, rychlé Apply na 2–4k ROI.
  • Kód (0–4): čisté oddělení UI/logic, LUTy, komentáře.
  • UX (0–4): srozumitelné názvy, bezpečné defaulty, Reset/Apply on copy.

9. Rozšíření (bonus)

  • Per-channel curves (R/G/B nezávisle) + přepínání zobrazení histogramu ROI.
  • Levels s černým/bílým bodem (auto na percentily, např. 1 % a 99 %).
  • Dithering při silném „posterize“ (Floyd–Steinberg).
  • Linear workflow: volitelný převod do lineárního prostoru (gamma 2.2 approximace) → aplikace křivek → zpět.

10. Troubleshooting

  • Změněná průhlednost — omylem měníte A; A vždy kopírujte.
  • „Zubaté“ křivky — chybná LUT nebo body mimo 0..255; zkontrolujte klipy a monotónnost (volitelně vynucujte).
  • Divné barvy po HSL — hlídejte hranice s,v (0..255), správný wrap h∈[0,360).
  • Pomalé preview — downsample, debounce; slučujte bloky (např. curves + sepia v jednom průchodu).

11. Kostra pluginu (zkráceno)

# color_lab.py
from krita import Extension
from PyQt5.QtWidgets import QDialog, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QSlider, QColorDialog

class ColorLab(Extension):
    def __init__(self, parent):
        super().__init__(parent)
        self.app = parent
    def setup(self): pass
    def createActions(self, window):
        act = window.createAction("pga_color_lab", "ColorLab (Curves/HSL/Sepia/Gradient)", "tools/scripts")
        act.triggered.connect(self.show_dialog)
    def show_dialog(self):
        dlg = ColorLabDialog(self.app)
        dlg.exec_()

class ColorLabDialog(QDialog):
    def __init__(self, app, parent=None):
        super().__init__(parent)
        self.app = app
        self.setWindowTitle("ColorLab")
        # TODO: vytvořit taby + ovládací prvky (viz sekce UI)
        # Přidat Preview/Apply/Apply on copy/Reset
        # Na Preview: zisk ROI→downsample→apply_pipeline(preview=True)→zobrazit QLabel
        # Na Apply: zisk ROI (plná velikost)→apply_pipeline(preview=False)→setPixelData→refresh
        # Pipeline: curves->HSL->sepia->gradient map
        # (viz implementační bloky výše)
        # ...
Důležité:

Stabilita & výkon: Dělejte co nejméně průchodů přes data. Ideál: sloučit vybrané bloky do jednoho průchodu tam, kde to jde (např. LUT curves + HSL + sepia mix v jedné smyčce). U velkých ROI ukažte QProgressDialog a v cyklu občas volejte QApplication.processEvents().

PGA — Krita Python Plugins: Cvičení 6 (BatchLab — hromadný export vrstev, sprite-sheet, balení a distribuce)

Autor: tým PGA :toc: macro

ZIP :toclevels: 3 :sectnums: :icons: font :source-highlighter: rouge :experimental: :lang: cs

Cíl cvičení: dokončit semestrální část tvorbou plně použitelných nástrojů pro automatizovaný export: 1) Batch export vrstev/skupin (více formátů, škálování, ořez průhlednosti, šablony názvů). 2) Sprite-sheet builder (mřížka i „tight“ ořez s paddingem + JSON/CSV manifest). 3) Release pluginu: metadata, ikona, README, krita-pip závislosti, jednoduché logování a nastavení.

1. Předpoklady

  • Umíte: číst/zapisovat pixely, respektovat BGRA, pracovat s ROI a výběrem.
  • Znáte: .desktop + init.py + createActions() + PyQt dialogy.
  • Doporučeno: projít Cvičení 2–5 (konvoluce, FFT, ColorLab).

2. Zadání (souhrn)

Vytvořte plugin BatchLab se dvěma hlavními kartami:

  • A) Batch Export
    • Scope: Selected Layers, Visible Layers, Top-level Groups, All.
    • Formáty: PNG (alfa), WebP, JPEG, TIFF (8/16-bit), volba kvality/komprese.
    • Škálování: 1× / 2× / 0.5× / vlastní procenta; volitelné přepočítat DPI.
    • Ořez průhlednosti („trim“): detekce bbox podle A kanálu; možnost přidat padding (px).
    • Šablona názvu souborů: např. {doc}_{layer}_{v:03d}@{scale}x.{ext} (viz níže).
    • Cíl exportu: výběr adresáře, volitelně create subfolder per doc.
  • B) Sprite-Sheet
    • Vstup: kolekce vrstev (nebo PNG v adresáři).
    • Layout: Grid (N×M) nebo Tight pack (řazení po řádcích, fixní dlaždice).
    • Tile size: auto (max(bbox_i)) nebo pevně W×H.
    • Padding: mezery (px) kolem dlaždic; volitelně extrude 1px (duplikace okraje pro lepší filtrování).
    • Výstup: atlas PNG + manifest (JSON/CSV) s pozicemi (x,y,w,h,name,anchor).
    • Volitelně: Power-of-Two canvas (1024, 2048…).
  • Common
    • Preview (rychlá suchá simulace rozložení/názvů).
    • Run s QProgressDialog; Cancel (bezpečné přerušení).
    • Log panel (stručné info, chyby).
    • Settings persist (QSettings) – poslední cesta, formát, šablona, atd.

3. UI návrh

  • Hlavní QTabWidget se záložkami Batch Export a Sprite-Sheet.
  • Vpravo dole: Preview, Run, Close. Vlevo dole: Open Output Folder.
  • Dole přes celou šířku: QPlainTextEdit („Log“), readonly, auto-scroll.
  • „i“ ikony s tooltipy u voleb (kvalita WebP/JPEG, význam paddingu apod.).

4. Export – šablona názvu souboru

Doporučené placeholdery:

PlaceholderPopis
{doc}Jméno dokumentu bez přípony
{layer}Jméno vrstvy/skupiny (sanitizované)
{idx}Pořadí v rámci exportu (1-based)
{scale}Měřítko (např. 1.0, 2.0)
{v:03d}Verze podle čísla v názvu dokumentu (detekce _v001)
{ext}Přípona podle zvoleného formátu (png, webp, …)

Příklad: {doc}_{layer}_{v:03d}@{scale}x.{ext}ui_icons_play_v007@2.0x.png

5. Práce s vrstvami a bbox (ořez podle A kanálu)

5.1. 4.1 Vyčtení projekce vrstvy a bbox

from krita import Krita
from PyQt5.QtGui import QImage
from PyQt5.QtCore import QRect

def node_projection_qimage(doc, node):
    # Získejte bounds vrstvy v dokumentu (globální souřadnice):
    b = node.bounds()
    x, y, w, h = b.x(), b.y(), b.width(), b.height()
    if w <= 0 or h <= 0:
        return None, QRect()
    # Získejte BGRA byty dané vrstvy (projekce včetně efektů):
    data = node.projectionPixelData(x, y, w, h)
    img = QImage(data, w, h, QImage.Format_ARGB32)  # BGRA↔ARGB32 byte-order
    return img.copy(), QRect(x, y, w, h)

def alpha_trim_bbox(qimg: QImage, threshold=1):
    # Najde minimální obdélník s alfou > threshold
    w, h = qimg.width(), qimg.height()
    if w==0 or h==0: return QRect(0,0,0,0)
    ptr = qimg.bits(); ptr.setsize(h*qimg.bytesPerLine())
    import numpy as np
    a = np.frombuffer(ptr, dtype=np.uint8).reshape(h, qimg.bytesPerLine())[:, 3::4]  # alfa kanál
    ys, xs = np.where(a > threshold)
    if xs.size == 0 or ys.size == 0:
        return QRect(0,0,0,0)
    x0, x1 = int(xs.min()), int(xs.max())
    y0, y1 = int(ys.min()), int(ys.max())
    return QRect(x0, y0, x1 - x0 + 1, y1 - y0 + 1)
Upozornění:

projectionPixelData() vrací projekci (vč. efektů). Pokud chcete čistě obsah vrstvy bez efektů, použijte pixelData(). Pozor na výkon u velmi velkých vrstev — u Batch Exportu preferujte export menších izolovaných vrstev oproti celé projekci plátna.

5.2. 4.2 Škálování, padding a uložení

from PyQt5.QtGui import QImage, QPainter
from PyQt5.QtCore import Qt

def pad_qimage(qimg: QImage, pad: int, color=(0,0,0,0)):
    if pad <= 0: return qimg
    w, h = qimg.width()+2*pad, qimg.height()+2*pad
    out = QImage(w, h, QImage.Format_ARGB32)
    out.fill(Qt.transparent)
    p = QPainter(out)
    p.drawImage(pad, pad, qimg)
    p.end()
    return out

def scale_qimage(qimg: QImage, scale: float):
    if abs(scale - 1.0) < 1e-6: return qimg
    w = max(1, int(round(qimg.width()*scale)))
    h = max(1, int(round(qimg.height()*scale)))
    # u ikon/UI preferujeme nejbližšího souseda:
    return qimg.scaled(w, h, Qt.IgnoreAspectRatio, Qt.FastTransformation)

5.3. 4.3 Export přes Krita API vs. Qt

  • Krita exportImage(path, InfoObject) – dobré pro celou projekci dokumentu (závisí na viditelnosti vrstev).
  • Qt QImage.save(path) – když si sami připravíte vyříznutý a škálovaný QImage z projekce vrstvy.

6. Batch Export – algoritmus

  1. Vyberte uzly podle „Scope“ (např. jen top-level layers v aktuální skupině).
  2. Pro každý uzel:
    1. Získejte projekci + bbox; aplikujte trim a padding.
    2. Aplikujte scale (1×, 2×, …).
    3. Vygenerujte název souboru dle šablony (sanitizovat /\:*?"<>|).
    4. Uložte QImage.save() (PNG/WebP/JPEG/TIFF) s nastavenou kvalitou/kompresí.
    5. Log: OK nebo chybová zpráva.
  3. Po dokončení: otevřete výstupní složku (volitelně).
Důležité:

PNG/WebP podporují alfa a hodí se pro UI/hry. JPEG nepodporuje průhlednost (zvažte volbu pozadí a flatten). TIFF můžete exportovat i 16-bit – hodí se pro tisk/archiv.

7. Sprite-Sheet – algoritmus (Grid / Tight pack)

7.1. 6.1 Grid mód

  • Zadejte cols × rows nebo automaticky spočítejte z počtu snímků.
  • tileW × tileH = buď auto (max bboxů) nebo fixní.
  • Výsledný canvas = cols*(tileW+2*pad) × rows*(tileH+2*pad) (plus extrude okrajů, pokud zapnete).
  • Každý snímek zarovnejte do dlaždice vlevo nahoře (nebo na střed) a uložte pozici do manifestu.

7.2. 6.2 Tight pack (jednoduché řádkování)

  • Seřaďte snímky podle šířky/součtu plochy, přidávejte je po řádcích, sledujte max výšku řádku.
  • Po ukončení řádku přidejte row padding.
  • Volitelně dorovnejte plátno na power-of-two.

7.3. 6.3 Manifest (JSON)

{
  "image": "atlas.png",
  "tileWidth":  64,
  "tileHeight": 64,
  "padding": 2,
  "frames": [
    {"name":"walk_000","x":0,"y":0,"w":64,"h":64,"anchor":[32,48]},
    {"name":"walk_001","x":66,"y":0,"w":64,"h":64,"anchor":[32,48]}
  ]
}

8. Kostra pluginu (zkráceně)

# batch_lab.py
from krita import Extension, Krita
from PyQt5.QtWidgets import (QDialog, QTabWidget, QWidget, QFileDialog, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QPushButton, QSpinBox, QDoubleSpinBox, QCheckBox,
                             QComboBox, QPlainTextEdit, QApplication, QProgressDialog)
from PyQt5.QtCore import Qt
import os, json, re

SAFE = re.compile(r'[^A-Za-z0-9._@\-+]')

class BatchLab(Extension):
    def __init__(self, parent): super().__init__(parent); self.app = parent
    def setup(self): pass
    def createActions(self, window):
        act = window.createAction("pga_batch_lab", "BatchLab (Export & SpriteSheet)", "tools/scripts")
        act.triggered.connect(self.show_dialog)
    def show_dialog(self):
        dlg = BatchLabDialog(self.app); dlg.exec_()

class BatchLabDialog(QDialog):
    def __init__(self, app, parent=None):
        super().__init__(parent); self.app = app
        self.setWindowTitle("BatchLab — Export & SpriteSheet")
        self.tabs = QTabWidget(self)
        self.tab_export = QWidget(); self.tab_sheet = QWidget()
        self.tabs.addTab(self.tab_export, "Batch Export")
        self.tabs.addTab(self.tab_sheet,  "Sprite-Sheet")
        # ... zde vytvořte ovládací prvky (scope, formát, scale, trim, padding, template, destFolder ...)
        # ... a u druhé karty (grid/tight, tileW/H, padding, pot, manifest path)
        self.log = QPlainTextEdit(); self.log.setReadOnly(True)
        btnPreview = QPushButton("Preview"); btnRun = QPushButton("Run"); btnClose = QPushButton("Close")
        btnPreview.clicked.connect(self.on_preview); btnRun.clicked.connect(self.on_run); btnClose.clicked.connect(self.close)
        lay = QVBoxLayout(self); lay.addWidget(self.tabs); lay.addWidget(self.log)
        row = QHBoxLayout(); row.addStretch(1); row.addWidget(btnPreview); row.addWidget(btnRun); row.addWidget(btnClose)
        lay.addLayout(row)
        # TODO: načíst QSettings (poslední cesta apod.)

    def logln(self, msg): self.log.appendPlainText(msg)

    def on_preview(self):
        # suchý průchod: spočítat názvy souborů / layout atlasu, nic neukládat
        self.logln("[Preview] ...")

    def on_run(self):
        doc = self.app.activeDocument()
        if not doc: self.logln("Žádný aktivní dokument."); return
        # připravit uzly dle scope, progress dialog, smyčka přes snímky/vrstvy
        # pro každý: projekce -> trim -> padding -> scale -> save -> záznam do JSON (u atlasu)
        self.logln("Hotovo.")

9. Ukládání nastavení a verze

  • Použijte QSettings (klíč „pga/batchlab/…“).
  • Nezapomeňte uložit template string, poslední dest folder, volby trim/scale/format.
  • Do README přidejte Release notes (verze, změny).

10. Balení a distribuce

  • Struktura:
my_batch_lab/
  my_batch_lab.desktop
  __init__.py
  batch_lab.py
  icons/  (32/64px PNG)
  README.md
  LICENSE
  • .desktop:
[Desktop Entry]
Type=Service
ServiceTypes=Krita/PythonPlugin
X-KDE-Library=my_batch_lab
X-Krita-Manual=Manual.html
Name=BatchLab (Export & SpriteSheet)
Comment=Batch export layers and build sprite sheets
  • Závislosti: pokud potřebujete externí balíčky (např. numpy), použijte krita-pip (viz cvičení 1 – Imports). U BatchLab by měl stačit čistý PyQt/Krita API.
  • Ikona: umístěte 32/64 px do icons/ a načtěte v UI (není povinné).

11. Výkon a stabilita (tipy)

  • Data-driven smyčky: minimalizujte počet konverzí → pracujte co nejdéle v QImage.
  • Trim + padding dělejte před škálováním (rychlejší, přesnější).
  • Názvy souborů sanitizujte (viz SAFE regex); u dlouhých jmen zkraťte (např. 80 znaků).
  • Zámky souborů / přepsání: kontrolujte existenci; nabídněte „overwrite / skip / rename“.
  • Cancel v QProgressDialog: po stisku okamžitě a bezpečně ukončete cyklus.
  • Power-of-Two: u atlasů pro některé enginy povinné → nabízí smysluplné volby (1024, 2048…).
  • Extrude okraje: duplikujte 1px rám (lepší bilineární/atlasové filtrování ve hře).

12. Odevzdání

  • Adresář pluginu my_batch_lab se zdrojáky (desktop, init, py, icons).
  • GIF/PNG ukázky:
    • Batch export – 3 ukázkové vrstvy před/po (trim+scale).
    • Sprite-sheet – atlas PNG + manifest JSON (2-3 řádky).
  • README s instalací, popisem voleb, známými limity, „Jak reportovat bug“.
  • (Volitelně) Manual.html – krátký tutoriál se screenshoty.

13. Checklist kvality

  • Export funguje pro Selected / Visible / All.
  • Škálování a trim se uplatní, padding a template generuje správné názvy.
  • Sprite-sheet: Grid + Tight pack, správné x,y,w,h v manifestu.
  • QProgressDialog + Cancel; UI nepadá, běh je logován.
  • Nastavení se pamatuje (QSettings).
  • Kód oddělený: IO / UI / layout / util funkce, komentáře.
  • README + .desktop + ikona; plugin se načte na Win/macOS/Linux.

14. Hodnocení (rubrika 0–4)

  • Funkčnost (0–4): export + atlas + manifest zcela funkční; korektní pojmenování.
  • UX (0–4): přehledné UI, srozumitelné volby, log, persist nastavení.
  • Výkon (0–4): plynulý běh na 100+ snímcích, rozumné využití paměti.
  • Kvalita kódu (0–4): struktura, komentáře, ošetření chyb, křížová kompatibilita.

15. Troubleshooting

  • Prázdný výstup / černé PNG: špatný bbox nebo nulová alfa → vypněte trim, zkontrolujte projectionPixelData.
  • Rozmazané ikony po škálování: pro UI používejte Qt.FastTransformation (nearest), pro fotky SmoothTransformation.
  • Špatné názvy souborů na Windows: znaky <>:"/\|?* → sanitizovat, nahradit _.
  • Rozhozený atlas: zkontrolujte stejné tileW/H v Grid módu, čiže auto bere max bbox.
  • Velké atlasy se nevejdou: zapněte power-of-two nebo zvyšte rozměr; nebo rozdělte do více atlasů.

16. Bonusy (rozšíření)

  • Více formátů naráz (PNG @1×, @2× + WebP).
  • Šablony presetů exportu (uložit/načíst JSON s konfigurací).
  • Per-layer overrides (např. custom anchor/metadata v názvu vrstvy #anchor=16,24).
  • DLL/Worker vlákno pro ukládání (QtConcurrent) – neblokuje UI.

17. Použití AI, citace a akademická integrita

AI nástroje (ChatGPT, generátory) jsou povoleny pro nápovědu a boilerplate. Zakázáno: odevzdat generovaný obsah bez vlastní práce, nebo použití cizích děl bez licence. Citace: U cizích zdrojů uvádějte autory/licence v README. Projekty s lidmi/brandem: Zvláštní pozornost k právům na podobu/loga.

18. Tahák zkratek (Krita)

AkceZkratka (Win/macOS)
Zvětšit/Zmenšit štětec[ / ]
Zrcadlení pohleduM
Rychlá maskaQ
KapátkoCtrl + Alt + klik / + + klik
Rychlý posun plátnaMMB drag / + Space + drag
Rotace/Reset plátnaShift + Space + drag / 5
Přepínání guma/štětecE
Skupiny vrstev – viditelnost/lockShift + klik na oko / zámek

19. Glosář pojmů (Krita/Blender/PGA)

BGRA
Byte-pořadí kanálů v Krita API (B,G,R,A). Alfa vždy kopírujte 1:1.
ROI
Region of Interest – oblast výběru. Preview běží na downsamplu ROI, Apply na plné ROI.
LUT
Look-Up Table (256 prvků/kanál) pro rychlé křivky/úrovně.
Hann okno
Okno pro FFT, tlumí zvonění po ostrých ořezech ve spektru.
Onion-skin
Průhledné zobrazení předchozích/následujících snímků v animaci.
PoT (Power-of-Two)
Velikost textury 2^n (1024, 2048…) – někdy vyžaduje engine.
Tight pack
Atlas skládající dlaždice s proměnlivou velikostí + manifest s pozicemi.