Tüm yazılar
Pazarlama 5 dk okuma

Visual Standards System: Kampanya Varlıklarında Marka Tutarlılığı

Marka kılavuzunu makine okunabilir kurallara çevirip kampanya görsellerinin renk, boyut ve format uyumunu Python ile otomatik denetleme yöntemi.

#brand-identity #design-system #visual-standards #campaign-management

Çoğu ekipte marka tutarlılığı bir PDF'e emanet edilir. Kılavuz hazırlanır, tasarımcıya gönderilir, bir süre herkes ona bakar; sonra kampanya dönemi gelir, ajans kendi görselini üretir, satış ekibi bir sunum şablonu çıkarır, stajyer bir story kesiti hazırlar ve kılavuz kimsenin artık açmadığı bir dosyaya dönüşür. Ben bu işe tasarımcı gözüyle değil, veri ve otomasyon gözüyle bakıyorum: bir marka kılavuzu belge değil, kurallar kümesidir. Kurallar kümesi olan her şey de makineye denetletilebilir.

Bu yazıda anlattığım yaklaşım şu: kılavuzdaki renk, boyut ve format kurallarını makine tarafından okunabilir bir dosyaya taşıyorum; sonra kampanya varlıklarını bu dosyaya karşı otomatik denetliyorum. Sistem estetik yargı vermiyor. "Bu görsel güzel mi?" sorusuna değil, "bu görsel kılavuzun dışına çıkmış mı?" sorusuna ölçülebilir bir cevap üretiyor.

Çalıştığım ekiplerde en sık gördüğüm sorun kötü tasarım değil, sessiz sapmaydı. Vurgu rengi birkaç ton kaymış, story görseli yanlış oranda kesilmiş, kılavuzdaki font yerine sistem fontu girmiş. Her biri tek başına küçük; toplamda markayı bulanıklaştıran şeyler. İnsan gözü bunları yorulunca kaçırır, kod yorulmaz.

Kılavuzu kurallar dosyasına taşımak

PDF'te "ana rengimiz lacivert" yazar. Kurallar dosyasında ise "ana renk #1F3A5F, izin verilen sapma Delta-E 8" yazar. Aradaki fark denetlenebilirliktir: ilk cümleyi yalnızca bir insan yorumlayabilir, ikincisini bir betik ölçebilir. Kılavuzdan kurallar dosyasına geçerken şunları taşıyorum:

  • Palet: her rengin adı ve hex kodu. Kılavuzda "yaklaşık" diye geçen ne varsa burada kesinleşmek zorunda.
  • Renk eşiği: bir pikselin palete "yeterince yakın" sayılması için üst sınır. Lab uzayında Delta-E 2,3 civarı insan gözünün fark edebildiği sınırdır; JPEG sıkıştırmasının yarattığı kaymayı da hoş görmek için ben 8 ile başlıyorum.
  • Asgari palet oranı: görselin yüzde kaçının palet renklerinden oluşması gerektiği. Fotoğraf ağırlıklı kanallarda bu değer düşer.
  • İhmal oranı: görselin çok küçük bir yüzdesini kaplayan renkler denetim dışı kalır. Bunu eklememin sebebi somut: kenar yumuşatma, iki palet rengi arasında hiçbir yerde kullanılmamış bir geçiş tonu üretir ve bu ton eşiği ihlal eder.
  • Kanal boyutları: her mecra için piksel ölçüleri.
  • İzinli formatlar: teslim edilebilecek dosya türleri.

Dosya JSON ya da YAML olabilir; önemli olan tek bir yerde durması ve sürüm takibine girmesi. Aşağıdaki kod hem kurallar dosyasını üretiyor hem de renk karşılaştırması için gereken uzay dönüşümünü tanımlıyor.

import json
import numpy as np

KURALLAR = {
    "palet": {
        "ana": "#1F3A5F",
        "vurgu": "#E8A33D",
        "zemin": "#F5F5F2",
        "metin": "#1A1A1A",
    },
    "renk_esigi": 8.0,          # Delta-E (CIE76) ust siniri
    "min_palet_orani": 0.60,    # piksellerin en az %60'i palette olmali
    "ihmal_orani": 0.02,        # %2'nin altindaki renkler (kenar yumusatma) sayilmaz
    "boyutlar": {
        "instagram_post": [1080, 1080],
        "instagram_story": [1080, 1920],
        "linkedin": [1200, 627],
    },
    "izinli_format": ["PNG", "JPEG"],
}

with open("marka-kurallari.json", "w", encoding="utf-8") as f:
    json.dump(KURALLAR, f, ensure_ascii=False, indent=2)


def hex_to_rgb(hex_str):
    h = hex_str.lstrip("#")
    return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))


def rgb_to_lab(rgb):
    """sRGB (0-255) -> CIE L*a*b*, D65 beyaz noktasi."""
    c = np.asarray(rgb, dtype=float) / 255.0
    c = np.where(c > 0.04045, ((c + 0.055) / 1.055) ** 2.4, c / 12.92)
    m = np.array([[0.4124, 0.3576, 0.1805],
                  [0.2126, 0.7152, 0.0722],
                  [0.0193, 0.1192, 0.9505]])
    xyz = (c @ m.T) / np.array([0.95047, 1.0, 1.08883])
    f = np.where(xyz > 0.008856, np.cbrt(xyz), 7.787 * xyz + 16 / 116)
    L = 116 * f[..., 1] - 16
    a = 500 * (f[..., 0] - f[..., 1])
    b = 200 * (f[..., 1] - f[..., 2])
    return np.stack([L, a, b], axis=-1)


def delta_e(lab1, lab2):
    """CIE76: Lab uzayinda Oklid mesafesi."""
    return float(np.linalg.norm(np.asarray(lab1) - np.asarray(lab2)))

Renk sapmasını ölçmek

Renkleri doğrudan RGB uzayında karşılaştırmak yanıltıcıdır; iki yeşil arasındaki aynı sayısal fark, iki mavi arasında gözle çok daha belirgin olabilir. Bu yüzden karşılaştırmayı algısal olarak düzgün olan Lab uzayında yapıyorum. Yukarıdaki CIE76 formülü en basit Delta-E ölçüsü; CIEDE2000 daha hassas ama ilk denetim kapısı için CIE76 fazlasıyla yeterli ve dışarıdan kütüphane gerektirmiyor.

Görselin baskın renklerini bulmak için Pillow'un median cut niceleyicisini kullanıyorum: görseli birkaç renge indirgiyor, her rengin piksel oranını alıyor ve her birini palete olan en yakın mesafesiyle etiketliyorum. Eşiği aşan her renk bir sapma kaydı oluyor.

from PIL import Image


def baskin_renkler(yol, adet=6):
    """Gorseli kucult, `adet` renge indirge, (rgb, oran) listesi dondur."""
    img = Image.open(yol).convert("RGB")
    img.thumbnail((200, 200))
    q = img.quantize(colors=adet, method=Image.Quantize.MEDIANCUT)
    palet = q.getpalette()[: adet * 3]
    sayim = sorted(q.getcolors(), reverse=True)   # (piksel_sayisi, indeks)
    toplam = sum(n for n, _ in sayim)
    return [(tuple(palet[i * 3:i * 3 + 3]), n / toplam) for n, i in sayim]


def renk_denetimi(yol, kurallar):
    izinli = {ad: rgb_to_lab(hex_to_rgb(hx))
              for ad, hx in kurallar["palet"].items()}
    esik = kurallar["renk_esigi"]
    uyumlu_oran = 0.0
    sapmalar = []
    for rgb, oran in baskin_renkler(yol):
        if oran < kurallar["ihmal_orani"]:
            continue    # kenar yumusatmadan dogan gecis renkleri
        lab = rgb_to_lab(rgb)
        en_yakin, mesafe = min(
            ((ad, delta_e(lab, ref)) for ad, ref in izinli.items()),
            key=lambda x: x[1],
        )
        if mesafe <= esik:
            uyumlu_oran += oran
        else:
            sapmalar.append({
                "renk": "#%02X%02X%02X" % rgb,
                "oran": round(oran, 3),
                "en_yakin": en_yakin,
                "delta_e": round(mesafe, 1),
            })
    return {
        "dosya": yol,
        "palet_orani": round(uyumlu_oran, 3),
        "gecti": uyumlu_oran >= kurallar["min_palet_orani"],
        "sapmalar": sapmalar,
    }

Çıktıdaki sapmalar listesi, tasarımcıya verilecek geri bildirimin kendisidir: "şu renk görselin %14'ünü kaplıyor, en yakın palet rengi vurgu, mesafe 19,4". Bu cümleyi bir insan yazsa on dakikasını alır; betik her görsel için aynı netlikte üretir.

Bir klasörü toplu denetlemek

Tek görseli denetlemek gösteri için iyidir; asıl değer, kampanya klasörünün tamamını tek komutla geçirip bir rapor üretmekte. Aşağıdaki denetleyici klasörü geziyor, her varlık için renk ve boyut kontrolü yapıyor, sonucu CSV'ye yazıyor. Mantık, daha önce Excel tablolarında kalite kontrolü otomatikleştirirken kullandığımla aynı: kuralı bir kez yaz, her kayda uygula, sapmayı listele.

import csv
import json
from pathlib import Path
from PIL import Image


def boyut_denetimi(yol, kurallar):
    with Image.open(yol) as img:
        w, h = img.size
        fmt = img.format
    kanal = [ad for ad, (bw, bh) in kurallar["boyutlar"].items()
             if (bw, bh) == (w, h)]
    return {
        "boyut": f"{w}x{h}",
        "kanal": kanal[0] if kanal else None,
        "format_ok": fmt in kurallar["izinli_format"],
    }


def toplu_denetim(klasor, kurallar_yolu="marka-kurallari.json",
                  rapor="denetim-raporu.csv"):
    with open(kurallar_yolu, encoding="utf-8") as f:
        kurallar = json.load(f)
    satirlar = []
    for yol in sorted(Path(klasor).rglob("*")):
        if yol.suffix.lower() not in {".png", ".jpg", ".jpeg"}:
            continue
        renk = renk_denetimi(str(yol), kurallar)
        boyut = boyut_denetimi(yol, kurallar)
        sorunlar = []
        if not renk["gecti"]:
            sorunlar.append(f"palet orani {renk['palet_orani']:.0%}")
        for s in renk["sapmalar"]:
            sorunlar.append(
                f"{s['renk']} (dE {s['delta_e']}, en yakin: {s['en_yakin']})")
        if boyut["kanal"] is None:
            sorunlar.append(f"boyut {boyut['boyut']} hicbir kanala uymuyor")
        if not boyut["format_ok"]:
            sorunlar.append("format izinli degil")
        satirlar.append({
            "dosya": yol.name,
            "kanal": boyut["kanal"] or "-",
            "durum": "SAPMA" if sorunlar else "GECTI",
            "sorunlar": "; ".join(sorunlar),
        })
    with open(rapor, "w", newline="", encoding="utf-8") as f:
        yazici = csv.DictWriter(
            f, fieldnames=["dosya", "kanal", "durum", "sorunlar"])
        yazici.writeheader()
        yazici.writerows(satirlar)
    sapma = sum(1 for s in satirlar if s["durum"] == "SAPMA")
    print(f"{len(satirlar)} varlik denetlendi, {sapma} sapma -> {rapor}")
    return satirlar


if __name__ == "__main__":
    toplu_denetim("kampanya/eylul")

Raporun CSV olması bilinçli bir tercih: pazarlama ekibi onu Sheets'te açıp filtreliyor, "SAPMA" satırlarını tasarımcıya geri gönderiyor. Denetim, karar vermiyor; kararın verileceği listeyi üretiyor.

Tipografi ve ikonografiyi denetlemek

Renk, rasterleşmiş görselden güvenilir biçimde okunabilen tek şey. Fontu bir PNG'den tespit etmeye çalışmak kırılgan bir iş; bu yüzden tipografi ve ikon denetimini piksel seviyesinde değil, kaynak dosya seviyesinde yapıyorum:

  • SVG ve HTML e-posta şablonları: dosyayı metin olarak açıp font-family değerlerini kurallar dosyasındaki izinli font listesiyle karşılaştırmak yeterli. Kılavuz dışı bir font adı geçiyorsa sapma.
  • Figma ve benzeri araçlar: tasarım API'sinden katman bilgisi çekilebiliyor; metin katmanlarının font ailesi ve ağırlığı oradan doğrudan okunur.
  • İkon seti: izinli ikonların dosya karmalarını (hash) tutuyorum. Kampanya klasöründe karması listede olmayan bir ikon dosyası varsa, biri kılavuz dışına çıkmış demektir.
  • Logo güvenli alanı: logonun kenarlara olan asgari uzaklığı kılavuzda yazıyorsa, logo dosyasının konumu şablon meta verisinden kontrol edilir.

Buradaki ilke, renk denetimindekiyle aynı: kuralı yorumlanabilir bir cümle olmaktan çıkarıp karşılaştırılabilir bir değere dönüştürmek. "Başlıklarda markanın fontu kullanılır" cümlesi denetlenemez; "başlık katmanlarında font-family şu üç değerden biri olmalı" cümlesi denetlenir.

Denetimi iş akışına bağlamak

Denetleyicinin masaüstünde elle çalıştırılan bir betik olarak kalması, PDF'teki kılavuzdan çok da farklı değildir; unutulur. Değer, akışın içine kapı olarak yerleştiğinde ortaya çıkıyor. Kullandığım üç yerleşim:

  • Paylaşılan klasör tetikleyicisi: ajans varlıkları teslim klasörüne bıraktığında betik çalışır, rapor aynı klasöre düşer. Kimse "denetimi çalıştırdın mı?" diye sormak zorunda kalmaz.
  • Yayın öncesi kapı: n8n ile kurduğum Instagram yayın akışında görsel, planlama adımına geçmeden önce denetimden geçer; sapma varsa akış durur ve tasarımcıya bildirim gider.
  • Sürüm takibi: kurallar dosyası git'te durur. Marka yenilendiğinde tek dosya değişir, sonraki denetimler yeni paleti kullanır ve eski paletle üretilmiş her varlık kendiliğinden sapma olarak görünür.

Bu yaklaşımın en somut faydası, gözden kaçan varyasyonları yakalaması: aynı vurgu rengini üç farklı ajans üç farklı hex koduyla üretmişse insan bunu bir bakışta fark edemez, denetim raporu üç satırda gösterir. Kılavuza uymayı da "tasarımcının vicdanı" olmaktan çıkarıp ölçülebilir bir teslim şartına dönüştürür; tartışma "bence uygun" ile "bence değil" arasında değil, bir sayı üzerinden yürür.

Marka kılavuzunu yazmak bir tasarım işidir; onu yaşatmak bir veri işi. Kılavuz bir kurallar dosyasına, uygunluk bir Delta-E değerine, teslim bir denetim raporuna dönüştüğünde tutarlılık niyet olmaktan çıkıp süreç haline geliyor. Danışmanlık işimin büyük kısmı tam olarak bu tür dönüşümlerden oluşuyor: belgede duran kuralı, sistemin içinde çalışan kurala çevirmek. Kampanya varlıklarınız için benzer bir denetim hattı kurmak istiyorsanız buradan ulaşabilirsiniz.