D
P
0
← Semua artikel Read in English

Layout, Overflow & Cascade CSS

Model Outpaint AI Malah Me-repaint Seluruh Gambar Sumbernya? Ukur PSNR-nya Dulu, Baru Pandu Pakai Blockout

· · 7 menit baca
Model Outpaint AI Malah Me-repaint Seluruh Gambar Sumbernya? Ukur PSNR-nya Dulu, Baru Pandu Pakai Blockout

Pekerjaannya kedengaran kecil: bikin strip sambungan yang memanjangkan satu foto ke bawah, supaya zona scroll berikutnya menyambung dari dunia yang sama. Rantai dunianya sudah ditetapkan sejak awal, dari gua ke pantai, lalu air gelap pre-dawn, kabut ivory, dan terakhir form. Konvensi geraknya juga sudah dikunci: dunia berikutnya masuk dari bawah viewport, jadi stripnya tumbuh ke bawah, bukan ke samping.

Versi pertama saya blend-nya rapi. Tidak ada garis di sambungan, tidak ada patahan. Klien tetap menolaknya, dan alasannya tidak ada hubungannya sama sekali dengan kualitas blend: "Abis garis pantai masa tiba tiba ada air lagi." Versi itu dia sebut kayak bug dan AI slop. Dan yang dia tunjuk itu benar: strip yang saya kirim meneruskan air terbuka di bawah garis pantai, mulus secara piksel tapi salah dunia.

Mulus belum berarti benar

Itu koreksi yang saya butuhkan. Kemulusan piksel bukan ukuran keberhasilan sebuah strip sambungan, karena mata pembaca tidak sedang mencari seam, dia sedang membaca sebuah tempat. Yang wajib dijaga adalah kontinuitas semantik, geografi dunianya, dan blend yang bersih sama sekali tidak bisa menutupi geografi yang salah.

Perbaikan prosesnya sederhana dan saya pakai terus sejak itu: sebelum menyentuh model apa pun, tulis dulu urutan geografi yang masuk akal sebagai kata, baru generate atau composite mengikuti urutan itu.

ombak -> garis buih -> pasir basah -> gelap

Empat kata itu murah dibuat dan mahal kalau dilewatkan. Begitu urutannya tertulis, saya punya alat untuk menolak hasil generate yang cantik tapi ngawur, dan nanti urutan yang sama juga jadi bahan mentah untuk blockout.

Tiga model, satu alat ukur yang sama

Masalah kedua muncul di lapisan yang berbeda, dan ini yang lebih mengejutkan buat saya. Model extend dan outpaint generatif tidak memperlakukan gambar sumber sebagai sesuatu yang tidak boleh diubah. Instruksi di prompt untuk mempertahankan sebagian area entah diabaikan sepenuhnya sehingga scene-nya di-repaint total, atau modelnya me-retone seluruh gambar.

Karena itu saya berhenti menilai hasil pakai mata dan mengukur seluruh alur kerja tiga bagian ini dengan PSNR terhadap gambar aslinya. PSNR bekerja pada skala logaritmik, 20 * log10(MAX / RMSE), jadi makin tinggi angkanya makin dekat hasil ke sumber, dan selisih beberapa dB artinya jauh lebih besar daripada yang terlihat dari angkanya.

Tiga model, tiga hasil yang berbeda jenisnya:

seedream_v5_pro berharga 3 kredit dan menerima --aspect_ratio 21:9|16:9 --resolution 2k. Untuk membuat frame baru dia bagus, palette golden-hour-nya presisi kalau warnanya disebut sebagai hex di prompt. Tapi diberi instruksi eksplisit "keep top exactly", dia tetap me-repaint seluruh scene. PSNR-nya terhadap original sekitar 16 dan komposisinya bergeser. Gejala yang sama saya catat di dua tempat terpisah.

Model outpaint berharga 2 kredit dan parameternya cuma medias dan aspect_ratio. Tidak ada parameter prompt sama sekali, yang justru menjelaskan banyak hal: tidak ada tempat untuk membujuknya. Dia meneruskan komposisi dengan benar dan menaruh gambar original di tengah keluarannya, tapi seluruh gambar di-retone secara global di PSNR sekitar 18. Splice mentah dari keluarannya memunculkan lompatan warna yang kelihatan.

nano_banana_2 juga 2 kredit dan preservasinya paling kuat, PSNR sekitar 28,6.

Selisihnya besar sekali begitu dibaca sebagai galat. Dari 18 ke 28,6 itu 10,6 dB, dan karena PSNR memakai 20 * log10, rasio galat RMS-nya 10^(10,6/20) yang kira-kira 3,4 kali lebih kecil. Dari 16 ke 28,6 itu 12,6 dB, atau 10^(12,6/20), sekitar 4,3 kali lebih kecil. Menjajal ketiganya sekali jalan cuma 3 + 2 + 2 = 7 kredit, dan itu murah dibanding menebak model mana yang menghormati sumbernya.

Kreditnya berbayar, jadi urutan perintahnya ada aturannya. Saya selalu cek saldo dulu, baca parameter model dari hf model get dan bukan model params yang tidak ada, lalu jalankan cost sebelum create.

hf account status
# sisa ~944 credits per 9 Jul 2026, plan max
 
hf model get outpaint
hf generate cost nano_banana_2 --prompt "..."
 
hf generate create nano_banana_2 \
  --prompt "render the blockout following the geography exactly" \
  --image blockout.png --wait --wait-timeout 5m
 
curl -o strip.png "https://<host-cloudfront>/..."

Dua detail yang menghemat waktu di sini. Media sebagai input dikirim lewat flag --image yang menerima uuid atau path berkas, sementara flag --medias yang berbentuk array object sering gagal lewat CLI. Dan hasilnya ditunggu dengan hf generate wait <job_id> atau flag --wait --wait-timeout 5m, yang mengembalikan URL cloudfront untuk diunduh pakai curl.

Blockout yang dipandu

Karena nano_banana_2 yang paling menghormati sumbernya, dia juga yang paling layak dikasih arahan. Dan arahan yang dia patuhi ternyata bukan kalimat, tapi gambar.

Tekniknya saya sebut guided blockout: area extension dikasari dulu dengan blok warna yang disampel dari fotonya sendiri, ditambah garis panduan putih di batas antar zona, baru dikirim dengan prompt "render the blockout following the geography exactly". Blok warna itu memberi tahu model warna apa yang harus ada di mana, dan garis putihnya memberi tahu di baris berapa satu zona berhenti dan zona berikutnya mulai.

Di sinilah urutan geografi yang tadi ditulis jadi kata berguna dua kali. Ombak, garis buih, pasir basah, gelap: itu daftar bloknya, dari atas ke bawah. Sejauh yang saya pakai, ini cara paling andal untuk mengontrol geografi hasil, jauh lebih andal daripada menambah kalimat ke prompt.

Tone-match sebelum splice

Kalau yang dipakai model outpaint, ada satu langkah yang tidak boleh dilewat. Karena dia me-retone global, splice langsung dari keluarannya akan kelihatan lompat warnanya. Untungnya dia menaruh original di tengah keluaran, jadi selalu ada zona overlap antara piksel asli dan versi yang sudah dia retone, dan zona itu yang dipakai untuk mencari koreksinya.

Koreksinya fit linear mean dan std per kanal RGB, dihitung hanya di zona overlap lalu diterapkan ke seluruh keluaran:

# untuk tiap kanal c di R, G, B, statistik diambil DARI ZONA OVERLAP saja
out[c] = (gen[c] - mean(gen_overlap[c])) / std(gen_overlap[c]) \
         * std(orig_overlap[c]) + mean(orig_overlap[c])

Dua parameter per kanal, enam angka total, dan lompatan warnanya hilang. Yang penting statistiknya diambil dari zona overlap, bukan dari seluruh gambar, karena bagian yang benar-benar bisa dibandingkan cuma bagian yang punya pasangan di kedua versi.

Menyusun stripnya

Setelah gambarnya benar, masih ada urusan menyambungkannya ke halaman. Aturan pertama saya: baris asli tetap opaque penuh sampai melewati baris edge terlihat maksimum. Untuk fit cover, baris itu dihitung edgeRow = 540 + (svh / 2) / scale, dan maksimumnya sekitar 1069 saat scale settle di 1,02.

Baru setelah baris itu saya pasang ramp alpha setebal 10 piksel, dan konten hasil generate ditaruh di bawahnya. Urutannya penting: kalau ramp-nya dimulai sebelum baris edge maksimum, ada kondisi scroll di mana pengguna melihat piksel hasil generate padahal seharusnya masih melihat foto aslinya.

Bagian generate-nya sendiri tidak dipetakan lurus. Ada kompresi perspektif progresif supaya teksturnya tetap terbaca aerial dan tidak melebar jadi tembok:

const edgeRow = 540 + (svh / 2) / scale;   // maks ~1069 saat scale settle 1.02
 
// 0 .. edgeRow            : baris asli, alpha penuh
// edgeRow .. edgeRow + 10 : ramp alpha
// di bawahnya             : hasil generate, dipetakan dengan
const src = H * (0.6 * t + 0.4 * t * t);   // t berjalan 0 -> 1

Efek kurva itu gampang diperiksa sendiri. Di t = 0,5, nilainya 0,6 * 0,5 + 0,4 * 0,25 = 0,4, jadi separuh strip yang atas cuma memakai 40 persen tinggi sumber dan separuh yang bawah harus memuat sisanya yang 60 persen. Kalau dibaca sebagai laju, turunannya bergerak dari 0,6 di t = 0 ke 1,4 di t = 1, artinya ekor strip menarik baris sumber 1,4 / 0,6 atau sekitar 2,33 kali lebih cepat daripada kepalanya. Itulah kompresinya. Penutup stripnya sendiri berupa ramp gelap, lalu feather alpha ke tekstur di bawahnya.

Strip tepi jangan sampai kena crop

Satu hal terakhir yang bisa membatalkan semua pekerjaan di atas dalam satu baris CSS. Kalau stripnya dipasang sebagai background dengan fit cover, sambungan yang sudah susah payah dihitung barisnya itu akan dipotong begitu rasio viewport berubah.

Jadi elemen tepi yang tidak boleh kepotong, termasuk strip sambungan ini, saya buat sebagai img dengan height: auto yang ditempel absolute di atas background texture cover yang memang crop-safe.

.seam-strip {
  position: absolute;
  width: 100%;
  height: auto; /* bukan object-fit: cover, barisnya harus utuh */
}

Teksturnya boleh kena crop karena tidak ada yang menghitung barisnya. Stripnya tidak boleh, karena seluruh gunanya ada di baris tertentu.

Pelajaran