BAB 24: Satu Perintah, Banyak Pekerjaan: NDVI, Validasi, dan Batch dengan Alat Processing
Studi kasus: "Hijaukah KPH kita, dan benarkah petanya?" #
Kepala Seksi minta tiga hal sekaligus. Pertama, peta kehijauan tanaman di KPH Contoh dari citra satelit, tanpa mengunduh seluruh citra. Kedua, angka akurasi: seberapa cocok peta klasifikasi dengan hasil survei lapangan? Ketiga, kehijauan dari puluhan foto drone, tanpa membuat QGIS macet. Analis Muda tidak mau mengklik alat satu per satu. Ia memanggil alatnya dari skrip, sehingga pekerjaan yang sama bisa diulang kapan saja.
Konsep: Processing dalam tiga kalimat #
QGIS punya ratusan alat analisis yang terkumpul di satu tempat bernama Processing. Bayangkan kasir di toko alat: Anda menyebut nama barang, menyerahkan formulir isian, lalu menerima barangnya. Di skrip, "menyebut nama barang" adalah processing.run("nama_alat", ...), dan formulirnya adalah sebuah kamus (dibahas di Bab 5).

Lima istilah baru di bab ini:
- Alat Processing (algorithm): satu alat analisis dengan nama tetap, misalnya
gdal:cliprasterbymasklayer. Awalan sebelum titik dua adalah penyedia alat:gdal,native,qgis, ataugrass. - Kamus parameter: formulir isian alat. Tiap isian punya nama (misalnya
'INPUT') dan nilai. - NDVI: angka kehijauan tanaman dari dua pita citra. Rumusnya
(NIR - Merah) / (NIR + Merah). Nilai tinggi berarti tanaman lebat. NIR adalah inframerah dekat. - COG (Cloud-Optimized GeoTIFF): citra GeoTIFF yang bisa dibaca sebagian lewat internet. Awalan
/vsicurl/pada alamat memberi tahu GDAL (perkakas pembaca raster) untuk membaca lewat internet. - Tugas latar belakang (background task): pekerjaan yang jalan di belakang layar, sehingga jendela QGIS tetap bisa dipakai.
Skrip 24.1: NDVI dari citra cloud (versi naskah) #
Berkas skrip_1_1.py. Di dalam berkasnya tertulis "Skrip 1.1". Skrip ini memotong dua pita citra lalu menghitung NDVI dengan qgis:rastercalculator. Pelajaran paling berharga di bab ini ada di dua baris terakhirnya: hasil alat Processing sering berupa teks jalur, bukan layer. Kita bedah sampai tuntas.
Tujuan. Memuat dua pita citra Sentinel-2 (Merah dan NIR) dari internet, memotongnya dengan batas KPH, lalu menghitung NDVI.
Bahan. Paket data paket-data: berkas Batas_KPH.gpkg (layer Batas_KPH, yang harus sudah dimuat di proyek) dan, sebagai pengganti internet, Sentinel_Lokal/B04.tif (Merah) serta B08.tif (NIR). Alamat citra pada skrip adalah contoh dan tidak benar-benar ada (lihat Jebakan), jadi untuk berlatih Anda mengganti baris 11 dan 12 dengan jalur berkas lokal.
Skrip memuat dua pita, memotong keduanya, lalu menghitung NDVI. Gambar alurnya ada di Skrip 24.2, yang memakai alat hitung lain di langkah akhir.
Skrip utuh:
# [SKRIP 1.1: Memuat COG & Eksekusi NDVI Virtual di QGIS 4.x]
# Penulis: Badar Mubarok Yogaswara
# Syarat: Pastikan layer batas area (misal: 'Batas_KPH') aktif di QGIS.
import processing
from qgis.core import QgsProject, QgsRasterLayer
print("🔗 Memulai koneksi ke Cloud-Optimized GeoTIFF (COG)...")
# 1. Tentukan URL COG dari STAC API (Contoh URL dummy Sentinel-2)
url_b4 = "/vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2026/T48M/B04.tif"
url_b8 = "/vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2026/T48M/B08.tif"
# 2. Muat Layer secara Virtual
layer_b4 = QgsRasterLayer(url_b4, "B04_Red_Cloud", "gdal")
layer_b8 = QgsRasterLayer(url_b8, "B08_NIR_Cloud", "gdal")
if not layer_b4.isValid() or not layer_b8.isValid():
print("❌ Gagal memuat citra dari Cloud. Cek koneksi internet.")
else:
QgsProject.instance().addMapLayers([layer_b4, layer_b8])
print("✅ Citra Red dan NIR berhasil dimuat.")
# 3. Ambil layer Vektor untuk batas pemotongan
batas_kph = QgsProject.instance().mapLayersByName('Batas_KPH')[0]
# 4. Potong (Clip) Citra langsung dari Cloud menggunakan Algoritma Native GDAL
print("✂️ Memotong citra berdasarkan batas KPH...")
# Clip Band 8 (NIR)
clip_b8 = processing.run("gdal:cliprasterbymasklayer", {
'INPUT': layer_b8,
'MASK': batas_kph,
'CROP_TO_CUTLINE': True,
'OUTPUT': 'TEMPORARY_OUTPUT'
})['OUTPUT']
# Clip Band 4 (Red)
clip_b4 = processing.run("gdal:cliprasterbymasklayer", {
'INPUT': layer_b4,
'MASK': batas_kph,
'CROP_TO_CUTLINE': True,
'OUTPUT': 'TEMPORARY_OUTPUT'
})['OUTPUT']
# 5. Kalkulasi NDVI Menggunakan Raster Calculator Native
# Rumus: (NIR - RED) / (NIR + RED)
print("📊 Menghitung NDVI...")
ndvi_result = processing.run("qgis:rastercalculator", {
'EXPRESSION': '("B08@1" - "B04@1") / ("B08@1" + "B04@1")',
'LAYERS': [clip_b8, clip_b4],
'CELLSIZE': 10,
'OUTPUT': 'TEMPORARY_OUTPUT'
})
QgsProject.instance().addMapLayer(QgsRasterLayer(ndvi_result['OUTPUT'], "NDVI_KPH", "gdal"))
print("🚀 Selesai! Layer NDVI_KPH telah ditambahkan ke kanvas.")Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-3 | # [SKRIP 1.1 ... | Komentar: judul, nama penulis, dan syarat: layer batas KPH harus sudah aktif di QGIS. |
| 5 | import processing | Panggil perkakas processing, pintu masuk ke semua alat Processing. |
| 6 | from qgis.core import QgsProject, QgsRasterLayer | Panggil dua perkakas QGIS: QgsProject untuk proyek yang sedang terbuka, QgsRasterLayer untuk membuka layer raster. |
| 8 | print("🔗 ...") | Tampilkan pesan awal. Tanda gambar (emoji) di dalam teks boleh saja, tetapi sebagian konsol menampilkannya sebagai kotak. |
| 11-12 | url_b4 = ..., url_b8 = ... | Simpan alamat dua pita citra di kotak url_b4 dan url_b8. Awalan /vsicurl/ berarti "baca lewat internet". B04 adalah pita Merah, B08 adalah NIR. |
| 15-16 | QgsRasterLayer(url_b4, "B04_Red_Cloud", "gdal") | Buka layer raster. Tiga isian: sumber data, nama layer di QGIS, dan pembaca raster "gdal". |
| 18 | if not layer_b4.isValid() or not layer_b8.isValid(): | Periksa: bila salah satu layer tidak sah (gagal dibuka), jalankan blok di bawahnya. or berarti "atau". |
| 19-20 | print("❌ ..."), else: | Bila gagal, cetak pesan galat. Bila berhasil, jalankan blok else (baris 21 dan seterusnya). |
| 21-22 | QgsProject.instance().addMapLayers([...]) | Tambahkan kedua layer ke proyek sekaligus. Tanda kurung siku membuat daftar berisi dua layer. |
| 25 | mapLayersByName('Batas_KPH')[0] | Cari layer bernama Batas_KPH di proyek. Hasilnya daftar, jadi [0] mengambil yang pertama. |
| 31-36 | clip_b8 = processing.run("gdal:cliprasterbymasklayer", {...})['OUTPUT'] | Panggil alat potong raster dengan batas KPH. Kamusnya memuat INPUT (raster), MASK (pemotong), CROP_TO_CUTLINE (pangkas tepi) dan OUTPUT. Hasilnya kamus, dan ['OUTPUT'] mengambil isi kunci OUTPUT. |
| 34 | 'CROP_TO_CUTLINE': True | Pangkas ukuran raster mengikuti batas pemotong. |
| 35 | 'OUTPUT': 'TEMPORARY_OUTPUT' | Simpan hasil di berkas sementara. Yang dikembalikan adalah teks jalur berkas itu. |
| 39-44 | clip_b4 = ... | Ulangi pemotongan untuk pita Merah. |
| 50-55 | processing.run("qgis:rastercalculator", {...}) | Hitung NDVI dengan kalkulator raster. EXPRESSION adalah rumus. Nama seperti "B08@1" berarti "pita 1 dari layer bernama B08". LAYERS adalah daftar layer yang dipakai. |
| 53 | 'CELLSIZE': 10 | Ukuran sel hasil: 10 satuan peta (meter pada data UTM). |
| 54 | 'OUTPUT': 'TEMPORARY_OUTPUT' | Simpan hasil di berkas sementara. Kembaliannya teks jalur berkas itu, bukan layer. Tulisan memory: tidak cocok untuk raster (lihat bawah). |
| 57 | addMapLayer(QgsRasterLayer(ndvi_result['OUTPUT'], "NDVI_KPH", "gdal")) | Bungkus teks jalur menjadi layer raster bernama NDVI_KPH, lalu tambahkan ke proyek. Tanpa pembungkus, addMapLayer menerima teks dan gagal. |
| 58 | print("🚀 ...") | Cetak pesan selesai. |
Hasil. Ada dua kemungkinan, bergantung pada baris 11-12.
Dengan alamat bawaan skrip (tidak diubah), layar menampilkan dua baris saja:
🔗 Memulai koneksi ke Cloud-Optimized GeoTIFF (COG)...
❌ Gagal memuat citra dari Cloud. Cek koneksi internet.Dengan citra lokal (baris 11-12 diganti jalur Sentinel_Lokal/B04.tif dan B08.tif), skrip berjalan sampai selesai:
🔗 Memulai koneksi ke Cloud-Optimized GeoTIFF (COG)...
✅ Citra Red dan NIR berhasil dimuat.
✂️ Memotong citra berdasarkan batas KPH...
📊 Menghitung NDVI...
🚀 Selesai! Layer NDVI_KPH telah ditambahkan ke kanvas.Layer NDVI_KPH muncul di proyek. Ukurannya 40 x 40 piksel, sebab pita masukan 200 x 200 piksel bersel 2 meter dan baris 53 meminta sel 10 meter. Pada uji ini, nilai NDVI terkecil 0,0194, terbesar 0,8246, rata-rata 0,5018. Hasil ini diuji pada QGIS 4.0.2 tanpa jendela, dengan data sintetis, jadi angkanya bukan kondisi lapangan.
Menyelidiki: apa isi hasil Processing? Fungsi addMapLayer minta sebuah layer. Namun ndvi_result['OUTPUT'] berisi teks. Buktinya bisa Anda lihat sendiri. Sisipkan dua baris ini tepat sebelum baris 57 (dengan jorokan yang sama):
print(ndvi_result)
print(type(ndvi_result['OUTPUT']))Baris pertama menampilkan kamus berisi jalur berkas sementara. Jalurnya berbeda di tiap komputer. Baris kedua menampilkan:
<class 'str'>Jadi alat mengembalikan teks ('str' singkatan string), yaitu jalur berkas hasil. Karena itu baris 57 membungkusnya dengan QgsRasterLayer.
Bagaimana bila baris 54 ditulis 'OUTPUT': 'memory:NDVI_KPH' dan baris 57 memakai addMapLayer(ndvi_result['OUTPUT'])? Uji menunjukkan alat tetap jalan tanpa galat. Namun kamus hasilnya {'OUTPUT': 'memory:NDVI_KPH'}, yang juga teks. Baris 57 lalu berhenti dengan TypeError: QgsProject.addMapLayer(): argument 1 has unexpected type 'str'. TypeError berarti "jenis nilainya salah". Efek sampingnya, sebuah berkas kosong bernama memory (0 byte) muncul di folder kerja. Untuk hasil vektor, tulisan memory: memang berfungsi (lihat Skrip 24.3). Untuk hasil raster, tidak.

Bagian yang boleh diubah.
- Baris 11-12, alamat citra. Ganti dengan jalur berkas lokal, misalnya
"D:/Latihan/paket-data/Sentinel_Lokal/B04.tif". Tanpa awalan/vsicurl/, karena berkasnya ada di komputer. - Baris 25, nama layer batas. Ganti
'Batas_KPH'dengan nama layer pemotong Anda. - Baris 53, ukuran sel. Ganti
10sesuai ukuran piksel citra Anda. - Baris 54, tempat hasil. Ganti
'TEMPORARY_OUTPUT'dengan jalur berkas tetap, misalnya'D:/Hasil/ndvi.tif', bila NDVI harus disimpan. Baris 57 tetap sama. Jangan memakaimemory:untuk raster.
Jebakan.
- Alamat contoh tidak ada. Saat diuji dengan perintah
curl, alamat pada baris 11 menjawab kode 404 ("tidak ditemukan"). Komentar skrip sendiri menyebutnya "URL dummy". Hasilnya, skrip berhenti di pesan "Gagal memuat citra". - Layer batas belum dimuat. Bila layer
Batas_KPHtidak ada di proyek, baris 25 gagal denganIndexError: list index out of range. Daftar hasil pencarian kosong, jadi[0]tidak punya isi. - Memakai
memory:untuk raster. Alat tidak galat, tetapi hasilnya teksmemory:NDVI_KPH, bukan layer. Galat baru muncul diaddMapLayer, beberapa baris sesudahnya. Pesan "Menghitung NDVI" tidak menjamin NDVI sudah ada. - Hasil Processing bukan selalu layer. Baris 31-36, 39-44, dan 50-55 mengembalikan teks jalur, bukan layer. Teks ini boleh dipakai sebagai sumber data, tetapi bukan sebagai layer. Itu sebabnya baris 57 membungkusnya.
Latihan variasi.
- Ganti alamat pada baris 11-12 dengan jalur berkas lokal.
- Tambahkan satu baris setelah baris 36 untuk melihat jenis hasil
clip_b8.
Contoh jawaban, hanya bagian yang berubah:
url_b4 = "D:/Latihan/paket-data/Sentinel_Lokal/B04.tif"
url_b8 = "D:/Latihan/paket-data/Sentinel_Lokal/B08.tif" print(type(clip_b8))Baris print menampilkan <class 'str'>: hasil pemotongan juga teks jalur.
Skrip 24.2: NDVI cloud, dengan layer bernama #
Berkas skrip_1_1_alternatif.py. Di dalam berkasnya tertulis "SKRIP 1.1 - ALTERNATIF". Perbedaannya hanya di langkah 5: kalkulator raster native:rastercalc (alat yang terdokumentasi) dengan layer yang diberi nama sendiri, dan hasilnya dimuat dari berkas.
Tujuan. Sama dengan Skrip 24.1, yaitu layer NDVI_KPH, tetapi dengan nama layer yang Anda atur sendiri.
Bahan. Sama dengan Skrip 24.1 (paket paket-data, layer Batas_KPH sudah dimuat, baris 13-14 diganti jalur lokal).

Skrip utuh:
# [SKRIP 1.1 - ALTERNATIF: Memuat COG & Eksekusi NDVI di QGIS 4.x]
# Penulis: Badar Mubarok Yogaswara
# Syarat: Pastikan layer batas area (misal: 'Batas_KPH') aktif di QGIS.
# Perbedaan dengan Skrip 1.1: langkah 5 memakai native:rastercalc (terdokumentasi) dan hasilnya
# disimpan ke berkas sementara lalu dimuat sebagai layer. Keluaran 'memory:' tidak menghasilkan layer raster.
import processing
from qgis.core import QgsProject, QgsRasterLayer
print("🔗 Memulai koneksi ke Cloud-Optimized GeoTIFF (COG)...")
# 1. Tentukan URL COG dari STAC API (Contoh URL dummy Sentinel-2)
url_b4 = "/vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2026/T48M/B04.tif"
url_b8 = "/vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2026/T48M/B08.tif"
# 2. Muat Layer secara Virtual
layer_b4 = QgsRasterLayer(url_b4, "B04_Red_Cloud", "gdal")
layer_b8 = QgsRasterLayer(url_b8, "B08_NIR_Cloud", "gdal")
if not layer_b4.isValid() or not layer_b8.isValid():
print("❌ Gagal memuat citra dari Cloud. Cek koneksi internet.")
else:
QgsProject.instance().addMapLayers([layer_b4, layer_b8])
print("✅ Citra Red dan NIR berhasil dimuat.")
# 3. Ambil layer Vektor untuk batas pemotongan
batas_kph = QgsProject.instance().mapLayersByName('Batas_KPH')[0]
# 4. Potong (Clip) Citra langsung dari Cloud menggunakan Algoritma Native GDAL
print("✂️ Memotong citra berdasarkan batas KPH...")
# Clip Band 8 (NIR)
clip_b8 = processing.run("gdal:cliprasterbymasklayer", {
'INPUT': layer_b8,
'MASK': batas_kph,
'CROP_TO_CUTLINE': True,
'OUTPUT': 'TEMPORARY_OUTPUT'
})['OUTPUT']
# Clip Band 4 (Red)
clip_b4 = processing.run("gdal:cliprasterbymasklayer", {
'INPUT': layer_b4,
'MASK': batas_kph,
'CROP_TO_CUTLINE': True,
'OUTPUT': 'TEMPORARY_OUTPUT'
})['OUTPUT']
# 5. Kalkulasi NDVI Menggunakan Raster Calculator Native
# Rumus: (NIR - RED) / (NIR + RED)
print("📊 Menghitung NDVI...")
# Beri nama pada hasil potongan agar bisa dirujuk di ekspresi sebagai "B08@1" dan "B04@1"
lyr_b8 = QgsRasterLayer(clip_b8, "B08", "gdal")
lyr_b4 = QgsRasterLayer(clip_b4, "B04", "gdal")
QgsProject.instance().addMapLayers([lyr_b8, lyr_b4], False)
ndvi_path = processing.run("native:rastercalc", {
'EXPRESSION': '("B08@1" - "B04@1") / ("B08@1" + "B04@1")',
'LAYERS': [lyr_b8, lyr_b4],
'OUTPUT': 'TEMPORARY_OUTPUT'
})['OUTPUT']
QgsProject.instance().addMapLayer(QgsRasterLayer(ndvi_path, "NDVI_KPH", "gdal"))
print("🚀 Selesai! Layer NDVI_KPH telah ditambahkan ke kanvas.")Penjelasan yang baru. Sampai baris 50, isi skrip ini sama dengan Skrip 24.1, hanya nomornya lebih besar dua karena komentar tambahan di baris 4-5. Karena itu hanya bagian baru yang dijelaskan. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 4-5 | # Perbedaan dengan Skrip 1.1 ... | Komentar penulis: langkah 5 diganti, dan ia sendiri mencatat bahwa keluaran memory: tidak menghasilkan layer raster. |
| 52 | # Beri nama pada hasil potongan ... | Komentar: nama layer harus cocok dengan nama di rumus. |
| 53-54 | lyr_b8 = QgsRasterLayer(clip_b8, "B08", "gdal") | Bungkus teks jalur hasil potongan menjadi layer sungguhan. Nama layer "B08" dan "B04" yang dipakai di rumus. |
| 55 | addMapLayers([lyr_b8, lyr_b4], False) | Daftarkan kedua layer ke proyek. Isian kedua, False, berarti jangan tampilkan di panel Layers. Layer tetap dikenali oleh nama. |
| 57-61 | processing.run("native:rastercalc", {...})['OUTPUT'] | Hitung NDVI dengan native:rastercalc. Ia tidak butuh CELLSIZE. Hasil ke TEMPORARY_OUTPUT, jadi kembaliannya teks jalur berkas sementara. |
| 63 | QgsRasterLayer(ndvi_path, "NDVI_KPH", "gdal") | Bungkus jalur hasil menjadi layer bernama NDVI_KPH, lalu addMapLayer memasukkannya ke peta. |
| 64 | print("🚀 ...") | Pesan selesai. |
Hasil. Dengan citra lokal pada baris 13-14, layar menampilkan:
🔗 Memulai koneksi ke Cloud-Optimized GeoTIFF (COG)...
✅ Citra Red dan NIR berhasil dimuat.
✂️ Memotong citra berdasarkan batas KPH...
📊 Menghitung NDVI...
🚀 Selesai! Layer NDVI_KPH telah ditambahkan ke kanvas.Layer NDVI_KPH muncul di proyek. Ukurannya 200 x 200 piksel dengan sel 2 meter, sama dengan pita masukan. Skrip 24.1 menghasilkan 40 x 40 piksel, sebab ia meminta sel 10 meter di CELLSIZE. Pemeriksaan tambahan pada uji ini: nilai NDVI terkecil 0,002, terbesar 0,836, rata-rata 0,505. Karena datanya sintetis, angka ini bukan kondisi lapangan.
Bagian yang boleh diubah.
- Baris 13-14, alamat citra. Jalur lokal atau alamat COG sungguhan.
- Baris 53-54, nama layer. Boleh diganti, asal sama dengan nama di rumus baris 58.
- Baris 58, rumus. Untuk indeks lain, ganti rumusnya. Contoh: selisih sederhana
"B08@1" - "B04@1". - Baris 60, tempat hasil. Ganti
'TEMPORARY_OUTPUT'dengan jalur berkas tetap bila hasil harus disimpan.
Jebakan.
- Nama layer di rumus tidak cocok. Rumus memanggil
"B08@1". Bila layer Anda dinamai lain, hasilnya tidak sah. Selalu samakan. - Lupa membungkus. Rumus memakai nama layer
B08. Teks jalurclip_b8tidak punya nama itu. Bila layer bernama B08 tidak terdaftar di proyek dan teks jalur dimasukkan langsung keLAYERS, hasilnyaQgsProcessingException: Error occurred while performing calculation.(diuji). - Butuh internet. Dengan alamat COG sungguhan, skrip bergantung pada koneksi dan pada alamat yang benar.
- Satuan sel.
native:rastercalcmemakai sel dari layer masukan. Cek ukurannya, jangan menebak.
Latihan variasi.
- Cetak nilai terkecil, terbesar, dan rata-rata NDVI di akhir skrip.
- Simpan hasil NDVI ke berkas tetap, bukan sementara.
Contoh jawaban 1, ditambahkan di akhir skrip:
ndvi = QgsProject.instance().mapLayersByName('NDVI_KPH')[0]
stat = ndvi.dataProvider().bandStatistics(1)
print("Min %.3f | maks %.3f | rata-rata %.3f" % (stat.minimumValue, stat.maximumValue, stat.mean))Layar menampilkan Min 0.002 | maks 0.836 | rata-rata 0.505.
Contoh jawaban 2: ganti baris 60 dengan jalur tetap. Berkas NDVI_KPH_tetap.tif terbentuk di folder tujuan, dan ndvi_path berisi jalur itu.
'OUTPUT': 'D:/Latihan/hasil/NDVI_KPH_tetap.tif'Skrip 24.3: Akurasi peta dari titik survei #
Berkas skrip_2_1.py. Di dalam berkasnya tertulis "SKRIP 2.1". Skrip ini mencocokkan hasil survei lapangan dengan kelas hasil klasifikasi citra. Di dunia penginderaan jauh, ini disebut validasi atau ground truth (kebenaran lapangan).
Tujuan. Menarik kelas dari poligon klasifikasi ke tiap titik survei, lalu menghitung berapa persen titik yang kelasnya cocok.
Bahan. Paket data paket-data: Survei.gpkg (layer Titik_Survei, 10 titik, kolom Kondisi) dan Poligon_Sawah.gpkg (layer Poligon_Sawah, 3 poligon, kolom Kelas_Citra). Kedua layer harus sudah dimuat di proyek dengan nama itu.
Skrip utuh:
# [SKRIP 2.1: Validasi Otomatis Titik Ground-Truth vs Poligon Klasifikasi]
# Penulis: Badar Mubarok Yogaswara
import processing
from qgis.core import QgsProject
print("🔍 Memulai Validasi Lapangan Otomatis...")
layer_survei = QgsProject.instance().mapLayersByName('Titik_Survei')[0]
layer_klasifikasi = QgsProject.instance().mapLayersByName('Poligon_Sawah')[0]
# Melakukan Spatial Join (Menarik atribut poligon ke titik survei yang berada di dalamnya)
hasil_join = processing.run("native:joinattributesbylocation", {
'INPUT': layer_survei,
'JOIN': layer_klasifikasi,
'PREDICATE': [0], # 0 = Intersects
'JOIN_FIELDS': ['Kelas_Citra'], # Field dari citra satelit
'METHOD': 0,
'DISCARD_NONMATCHING': False,
'PREFIX': 'GIS_',
'OUTPUT': 'memory:Validasi_Hasil'
})['OUTPUT']
# Menambahkan layer hasil ke QGIS
QgsProject.instance().addMapLayer(hasil_join)
# Menghitung Akurasi
titik_cocok = 0
total_titik = hasil_join.featureCount()
for fitur in hasil_join.getFeatures():
if str(fitur['Kondisi']).lower() == str(fitur['GIS_Kelas_Citra']).lower():
titik_cocok += 1
akurasi = (titik_cocok / total_titik) * 100 if total_titik > 0 else 0
print(f"📊 Laporan Validasi: {titik_cocok} dari {total_titik} titik cocok.")
print(f"🎯 Akurasi Keseluruhan (Overall Accuracy): {akurasi:.2f}%")Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-2 | # [SKRIP 2.1 ... | Komentar: judul dan nama penulis. |
| 4-5 | import processing, from qgis.core import QgsProject | Panggil perkakas Processing dan perkakas proyek. |
| 7 | print("🔍 ...") | Pesan awal. |
| 9-10 | mapLayersByName('Titik_Survei')[0] | Ambil layer titik survei dan layer poligon dari proyek, masing-masing ke satu kotak. |
| 13-22 | processing.run("native:joinattributesbylocation", {...})['OUTPUT'] | Gabungkan atribut berdasarkan letak: tiap titik menerima isi kolom dari poligon yang memuatnya. Hasilnya diambil dari kunci OUTPUT. |
| 14-15 | 'INPUT', 'JOIN' | Layer yang menerima (titik) dan layer pemberi (poligon). |
| 16 | 'PREDICATE': [0] | Syarat letak. Isiannya daftar. 0 berarti "bersinggungan atau berada di dalam" (intersects). |
| 17 | 'JOIN_FIELDS': ['Kelas_Citra'] | Hanya kolom ini yang dibawa dari poligon. |
| 18 | 'METHOD': 0 | Satu titik boleh menerima banyak poligon cocok, masing-masing menjadi satu baris. |
| 19 | 'DISCARD_NONMATCHING': False | Titik yang tidak jatuh di poligon mana pun tetap disimpan, dengan isi kosong. |
| 20 | 'PREFIX': 'GIS_' | Awalan nama kolom yang dibawa. Kolom baru bernama GIS_Kelas_Citra. |
| 21 | 'OUTPUT': 'memory:Validasi_Hasil' | Simpan hasil di memori dengan nama Validasi_Hasil. Untuk hasil vektor, ini berfungsi dan mengembalikan layer. |
| 25 | addMapLayer(hasil_join) | Masukkan layer hasil ke proyek. |
| 28-29 | titik_cocok = 0, total_titik = hasil_join.featureCount() | Siapkan penghitung cocok di angka 0, lalu hitung jumlah titik. |
| 31-33 | for fitur in hasil_join.getFeatures(): | Ulangi untuk tiap titik. Bila kondisi survei sama dengan kelas citra, tambah penghitung dengan += 1. |
| 32 | str(...).lower() | Ubah isi kolom menjadi teks huruf kecil, supaya Sehat dan sehat dianggap sama. |
| 35 | akurasi = (...) * 100 if total_titik > 0 else 0 | Hitung persen. Bagian if ... else 0 mencegah pembagian dengan nol bila tidak ada titik. |
| 37-38 | print(f"...") | Cetak laporan. Huruf f di depan kutip membuat teks di dalam kurung kurawal diganti nilainya (dibahas di Bab 9). :.2f berarti dua angka desimal. |
Hasil. Layar menampilkan:
🔍 Memulai Validasi Lapangan Otomatis...
📊 Laporan Validasi: 9 dari 10 titik cocok.
🎯 Akurasi Keseluruhan (Overall Accuracy): 90.00%Layer Validasi_Hasil muncul di proyek dengan kolom fid, Kondisi, Tinggi_Phn, Foto, dan GIS_Kelas_Citra. Satu titik sengaja dibuat tidak cocok, jadi 9 dari 10.
Bagian yang boleh diubah.
- Baris 9-10, nama layer. Ganti dengan nama layer Anda. Harus sama persis dengan nama di panel Layers.
- Baris 17, kolom yang dibawa. Ganti
Kelas_Citradengan kolom poligon lain. - Baris 20 dan 32, awalan. Bila awalan diganti, nama kolom di baris 32 harus ikut diganti.
- Baris 16, syarat letak. Isiannya daftar kode. Menurut daftar pilihan alat di QGIS 4.0.2:
0bersinggungan (intersect),1memuat,2sama,3bersentuhan,4tumpang tindih,5berada di dalam,6menyilang.
Jebakan.
- Awalan tidak sejalan. Bila baris 20 diganti
'CITRA_'tetapi baris 32 dibiarkan, layar menampilkanKeyError: 'GIS_Kelas_Citra'. Kolom itu tidak ada lagi. - Nama layer salah. Layer tidak ditemukan, daftar kosong, dan baris 9 berhenti dengan
IndexError: list index out of range. - Titik di luar poligon. Titik itu bernilai kosong dan dihitung tidak cocok. Angka akurasi turun tanpa peringatan.
- Banyak poligon cocok. Dengan
METHOD: 0, titik yang jatuh di dua poligon tumpang tindih menjadi dua baris, sehinggatotal_titikbisa lebih besar dari jumlah titik asli. [kemungkinan, tidak diuji]
Latihan variasi.
- Tampilkan kondisi dan kelas citra untuk titik yang tidak cocok.
- Ganti awalan menjadi
CITRA_dengan benar.
Contoh jawaban 1, ditambahkan di akhir skrip:
for fitur in hasil_join.getFeatures():
if str(fitur['Kondisi']).lower() != str(fitur['GIS_Kelas_Citra']).lower():
print("TIDAK COCOK:", fitur['Kondisi'], "|", fitur['GIS_Kelas_Citra'])Layar menampilkan TIDAK COCOK: Terserang Hama | Sehat. Titik itu disurvei sebagai terserang hama, tetapi poligon citranya "sehat".
Contoh jawaban 2: ganti baris 20 menjadi 'PREFIX': 'CITRA_' dan baris 32 menjadi fitur['CITRA_Kelas_Citra']. Hasilnya tetap 9 dari 10 titik cocok.
Skrip 24.4: NDVI banyak ubin di latar belakang #
Berkas skrip_3_1.py. Di dalam berkasnya tertulis "SKRIP 3.1". Drone menghasilkan puluhan ubin foto. Menghitung semuanya di jendela utama membuat QGIS membeku. Solusinya: jalankan sebagai tugas latar belakang.
Tujuan. Menghitung NDVI untuk semua berkas .tif di satu folder, satu per satu di belakang layar, lalu mencatat hasilnya di log.
Bahan. Paket data paket-data, folder Ortofoto_Mentah berisi ubin_01.tif sampai ubin_03.tif (tiap berkas 2 pita: pita 1 NIR, pita 2 Merah; ukuran 100 x 100 piksel). Folder keluaran, misalnya NDVI_Hasil, dibuat otomatis oleh skrip bila belum ada. Jalur di baris 8 dan 9 adalah jalur komputer penulis, jadi Anda wajib menggantinya.

Skrip utuh:
# [SKRIP 3.1: Batch Processing Citra Drone di Latar Belakang (Non-Blocking)]
# Penulis: Badar Mubarok Yogaswara
import os
import processing
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis
folder_input = "D:/Data_Drone/Ortofoto_Mentah/"
folder_output = "D:/Data_Drone/NDVI_Hasil/"
def proses_batch_ndvi(task, folder_in, folder_out):
QgsMessageLog.logMessage("Memulai Batch Processing di Latar Belakang...", "Analisis Drone", Qgis.Info)
os.makedirs(folder_out, exist_ok=True)
file_list = [f for f in os.listdir(folder_in) if f.endswith('.tif')]
total_files = len(file_list)
for i, file_name in enumerate(file_list):
if task.isCanceled():
return False
path_in = os.path.join(folder_in, file_name)
path_out = os.path.join(folder_out, f"NDVI_{file_name}")
processing.run("qgis:rastercalculator", {
'EXPRESSION': '("{}@1" - "{}@2") / ("{}@1" + "{}@2")'.format(path_in, path_in, path_in, path_in),
'LAYERS': [path_in],
'OUTPUT': path_out
})
if not os.path.exists(path_out):
QgsMessageLog.logMessage(f"Keluaran tidak terbentuk: {path_out}", "Analisis Drone", Qgis.Critical)
return False
task.setProgress((i + 1) / total_files * 100)
return True
def task_selesai(exception, result=None):
if result:
QgsMessageLog.logMessage("✅ Batch Processing Selesai 100%!", "Analisis Drone", Qgis.Success)
else:
QgsMessageLog.logMessage("❌ Proses dibatalkan atau error.", "Analisis Drone", Qgis.Warning)
task_ndvi = QgsTask.fromFunction("Kalkulasi NDVI 50 Ubin", proses_batch_ndvi,
on_finished=task_selesai,
folder_in=folder_input, folder_out=folder_output)
QgsApplication.taskManager().addTask(task_ndvi)Penjelasan baris demi baris. Skrip ini punya dua fungsi (dibahas di Bab 7) dan satu pemanggil. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-2 | # [SKRIP 3.1 ... | Komentar: judul dan nama penulis. |
| 4-6 | import os, import processing, from qgis.core import ... | Panggil os (urusan berkas dan folder), processing, dan empat perkakas QGIS untuk tugas dan log. |
| 8-9 | folder_input = "D:/Data_Drone/Ortofoto_Mentah/" | Dua kotak berisi lokasi folder masukan dan keluaran. Ini jalur komputer penulis dan harus diganti. |
| 11 | def proses_batch_ndvi(task, folder_in, folder_out): | Buat fungsi (resep) bernama proses_batch_ndvi. Ia menerima tiga isian: objek tugas, folder masukan, folder keluaran. |
| 12 | QgsMessageLog.logMessage(..., "Analisis Drone", Qgis.Info) | Tulis catatan ke panel log QGIS, di tab bernama Analisis Drone. |
| 13 | os.makedirs(folder_out, exist_ok=True) | Buat folder keluaran bila belum ada. exist_ok=True berarti "jangan galat bila folder sudah ada" (lihat Bab 8). Tanpa baris ini, folder yang tidak ada membuat GDAL gagal menulis. |
| 15 | file_list = [f for f in os.listdir(folder_in) if f.endswith('.tif')] | Kumpulkan nama berkas di folder masukan yang berakhiran .tif (daftar dengan syarat, dibahas di Bab 6). Huruf besar dan kecil dibedakan. |
| 16 | total_files = len(file_list) | Hitung jumlah berkas. |
| 18 | for i, file_name in enumerate(file_list): | Ulangi untuk tiap berkas. enumerate menambah nomor urut i mulai dari 0. |
| 19-20 | if task.isCanceled(): return False | Bila pengguna membatalkan tugas, berhenti dan kembalikan False. |
| 22-23 | path_in = os.path.join(...), path_out = ... | Susun jalur lengkap berkas masukan dan keluaran. Nama keluaran diawali NDVI_ lewat f-string. |
| 25-29 | processing.run("qgis:rastercalculator", {...}) | Hitung NDVI untuk satu ubin. |
| 26 | '("{}@1" - "{}@2") / ("{}@1" + "{}@2")'.format(path_in, ...) | Rumus NDVI. Pita 1 adalah NIR dan pita 2 Merah. Empat tanda {} diisi jalur berkas, sebab di sini nama layer adalah jalur berkas itu sendiri. |
| 27 | 'LAYERS': [path_in] | Layer yang dipakai, ditulis sebagai jalur berkas. |
| 28 | 'OUTPUT': path_out | Simpan hasil ke jalur keluaran. |
| 31-33 | if not os.path.exists(path_out): | Periksa apakah berkas hasil benar-benar terbentuk (os.path.exists, lihat Bab 8). Bila tidak, tulis pesan tingkat Qgis.Critical (paling berat) ke log, lalu return False menghentikan tugas. |
| 35 | task.setProgress((i + 1) / total_files * 100) | Laporkan kemajuan dalam persen ke QGIS. |
| 37 | return True | Tanda tugas selesai tanpa dibatalkan dan tanpa kegagalan. |
| 39-43 | def task_selesai(exception, result=None): | Fungsi penutup. Bila result benar (True), catat sukses. Bila tidak, catat peringatan. Isian kedua diberi nilai bawaan result=None karena pada jalur gagal (tugas mengembalikan False atau galat) QGIS memanggil fungsi ini hanya dengan satu isian, exception. Tanpa nilai bawaan itu, pesan peringatan tidak pernah tercetak. |
| 45-47 | QgsTask.fromFunction("Kalkulasi NDVI 50 Ubin", proses_batch_ndvi, ...) | Bungkus fungsi menjadi tugas. Isian: nama tugas, fungsi kerja, fungsi penutup on_finished, dan dua isian kata kunci untuk folder. |
| 49 | QgsApplication.taskManager().addTask(task_ndvi) | Serahkan tugas ke pengelola tugas QGIS. Sejak baris ini, tugas jalan di belakang layar. |
Hasil. Panel log QGIS (menu View, Panels, Log Messages), tab Analisis Drone, memuat dua pesan. Tiap pesan di panel diawali keterangan waktu. [CEK]
Memulai Batch Processing di Latar Belakang...
✅ Batch Processing Selesai 100%!Folder keluaran berisi tiga berkas:
NDVI_ubin_01.tif 41284 byte
NDVI_ubin_02.tif 41284 byte
NDVI_ubin_03.tif 41284 bytePemeriksaan pada uji ini: ketiga hasil dibandingkan dengan hitungan NumPy (NIR - Merah) / (NIR + Merah). Selisih terbesar hanya 0,00000003 (pembulatan angka desimal). Nilai NDVI tiap ubin berkisar 0,002 sampai 0,817.
Bagian yang boleh diubah.
- Baris 8-9, folder. Ganti dengan folder di komputer Anda, dengan garis miring biasa
/dan garis miring di akhir. - Baris 15, jenis berkas. Ganti
'.tif'bila berkas Anda berekstensi lain. - Baris 26, pita. Tukar
@1dan@2bila urutan pita di data Anda terbalik. - Baris 45, nama tugas. Ganti teks bebas. Tulisan "50 Ubin" adalah tulisan tetap, tidak dihitung dari data.
- Baris 13 dan 31-33, pengaman. Biarkan keduanya. Baris 13 membuat folder keluaran, dan baris 31-33 menghentikan tugas bila satu hasil tidak terbentuk.
Jebakan.
- Folder keluaran tidak ada. Baris 13 membuatnya otomatis. Tanpa baris itu, uji menunjukkan hal yang menipu: log tetap menulis "Batch Processing Selesai 100%!", padahal tidak ada berkas terbentuk. Di terminal tampil
ERROR 4: Attempt to create new tiff file ... failed ... No such file or directory. Karena itu baris 31-33 memeriksa berkas hasil. Tetap periksa isi folder keluaran. - Satu ubin gagal, sisanya tidak diproses. Pada baris 31-33,
return Falsemengakhiri tugas. Ubin yang belum dikerjakan dilewati. Cek log untuk tahu ubin mana yang gagal. - Jalur penulis. Bila baris 8-9 tidak diganti dan folder masukan tidak ada, tugas berhenti karena galat
FileNotFoundError. Rincian galat itu tidak tampil di log: yang terlihat pesan "Memulai Batch Processing di Latar Belakang...", lalu "❌ Proses dibatalkan atau error." (peringatan). Lebih dulu, baris 13 sempat membuat folder keluaran. Bila driveD:ada di komputer Anda, folderD:/Data_Drone/NDVI_Hasil/bisa terbentuk di tempat yang tidak Anda maksud (diuji pada folder tiruan, bukan pada driveD:sungguhan). Bila folder masukan ada tetapi kosong, log tetap menulis "Selesai 100%" tanpa membuat berkas. Ini latihan mengganti jalur yang paling sering Anda lakukan. - Ekstensi huruf besar. Berkas
ubin_01.TIFterlewat, sebab.tifpeka huruf besar dan kecil. - Pesan galat palsu di log. Pada uji ini, log juga memuat pesan
Cannot open <jalur berkas> ().()dari pembaca data, tetapi hasilnya tetap benar. Penyebabnya nama layer berupa jalur berkas. Skrip 24.5 menghindarinya. - Jalur dengan spasi atau tanda aneh. Nama layer berupa jalur berkas rawan salah bila jalurnya memuat spasi. [kemungkinan, tidak diuji]
- Pesan hanya di log. Tidak ada tulisan di konsol. Buka panel log untuk melihat kemajuan.
Latihan variasi.
- Ganti jalur folder sesuai komputer Anda.
- Tambahkan catatan log untuk tiap berkas yang diproses.
Contoh jawaban 2: sisipkan satu baris sebelum baris 25 (tepat sesudah path_out), dengan jorokan yang sama.
QgsMessageLog.logMessage(f"Memproses {file_name}", "Analisis Drone", Qgis.Info)Log menampilkan Memproses ubin_01.tif, Memproses ubin_02.tif, dan Memproses ubin_03.tif di antara pesan awal dan pesan selesai.
Skrip 24.5: Batch NDVI, versi dengan layer bernama #
Berkas skrip_3_1_alternatif.py. Di dalam berkasnya tertulis "SKRIP 3.1 - ALTERNATIF". Skrip ini mengerjakan hal yang sama dengan Skrip 24.4, tetapi memakai native:rastercalc dan layer yang diberi nama. Karena itu hanya perbedaannya yang dijelaskan.
Tujuan. Sama dengan Skrip 24.4, dengan cara yang lebih rapi dan terdokumentasi.
Bahan. Sama dengan Skrip 24.4: folder Ortofoto_Mentah, folder keluaran, dan jalur di baris 10-11 yang harus diganti.
Skrip utuh:
# [SKRIP 3.1 - ALTERNATIF: Batch Processing Citra Drone di Latar Belakang (Non-Blocking)]
# Penulis: Badar Mubarok Yogaswara
# Perbedaan dengan Skrip 3.1: memakai native:rastercalc (terdokumentasi) dengan layer bernama,
# bukan qgis:rastercalculator dengan nama layer berupa jalur berkas.
import os
import processing
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis, QgsRasterLayer
folder_input = "D:/Data_Drone/Ortofoto_Mentah/"
folder_output = "D:/Data_Drone/NDVI_Hasil/"
def proses_batch_ndvi(task, folder_in, folder_out):
QgsMessageLog.logMessage("Memulai Batch Processing di Latar Belakang...", "Analisis Drone", Qgis.Info)
os.makedirs(folder_out, exist_ok=True)
file_list = [f for f in os.listdir(folder_in) if f.endswith('.tif')]
total_files = len(file_list)
for i, file_name in enumerate(file_list):
if task.isCanceled():
return False
path_in = os.path.join(folder_in, file_name)
path_out = os.path.join(folder_out, f"NDVI_{file_name}")
# Beri nama layer agar bisa dirujuk di ekspresi
layer_in = QgsRasterLayer(path_in, "ortofoto", "gdal")
try:
processing.run("native:rastercalc", {
'EXPRESSION': '("ortofoto@1" - "ortofoto@2") / ("ortofoto@1" + "ortofoto@2")',
'LAYERS': [layer_in],
'OUTPUT': path_out
})
except Exception as e:
QgsMessageLog.logMessage(f"Gagal memproses {file_name}: {e}", "Analisis Drone", Qgis.Critical)
return False
if not os.path.exists(path_out):
QgsMessageLog.logMessage(f"Keluaran tidak terbentuk: {path_out}", "Analisis Drone", Qgis.Critical)
return False
task.setProgress((i + 1) / total_files * 100)
return True
def task_selesai(exception, result=None):
if result:
QgsMessageLog.logMessage("✅ Batch Processing Selesai 100%!", "Analisis Drone", Qgis.Success)
else:
QgsMessageLog.logMessage("❌ Proses dibatalkan atau error.", "Analisis Drone", Qgis.Warning)
task_ndvi = QgsTask.fromFunction("Kalkulasi NDVI 50 Ubin", proses_batch_ndvi,
on_finished=task_selesai,
folder_in=folder_input, folder_out=folder_output)
QgsApplication.taskManager().addTask(task_ndvi)Penjelasan yang baru. Bagian lain sama dengan Skrip 24.4, hanya nomor barisnya bergeser. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 3-4 | # Perbedaan dengan Skrip 3.1 ... | Komentar penulis: kini memakai native:rastercalc dengan layer bernama, bukan nama layer berupa jalur berkas. |
| 8 | ... Qgis, QgsRasterLayer | Satu perkakas ditambahkan: QgsRasterLayer. |
| 15 | os.makedirs(folder_out, exist_ok=True) | Buat folder keluaran bila belum ada, sama seperti Skrip 24.4. |
| 27-28 | layer_in = QgsRasterLayer(path_in, "ortofoto", "gdal") | Buka tiap ubin sebagai layer bernama ortofoto. Nama sama dipakai untuk semua ubin, sebab layer dibuat baru di tiap putaran. |
| 30-38 | try: ... except Exception as e: | Coba jalankan perhitungan. Bila terjadi galat jenis apa pun (Exception), loncat ke blok except (lihat Bab 10). Pesannya tersimpan di e. |
| 31-35 | processing.run("native:rastercalc", {...}) | Rumus memakai "ortofoto@1" dan "ortofoto@2", jauh lebih pendek daripada rumus berisi jalur. LAYERS berisi layer, bukan jalur. |
| 37-38 | QgsMessageLog.logMessage(f"Gagal memproses {file_name}: {e}", ...), return False | Catat nama ubin dan pesan galatnya sebagai Qgis.Critical, lalu hentikan tugas. |
| 40-42 | if not os.path.exists(path_out): | Sama seperti Skrip 24.4: periksa bahwa berkas hasil benar-benar ada. |
Hasil. Log menampilkan dua pesan yang sama seperti Skrip 24.4. Tiga berkas NDVI_ubin_01.tif sampai NDVI_ubin_03.tif terbentuk, masing-masing 41284 byte. Pesan Cannot open ... tidak muncul lagi pada uji ini.
Bagian yang boleh diubah. Baris 10-11 (folder), baris 17 (jenis berkas), dan baris 32 (rumus), seperti pada Skrip 24.4. Satu tambahan: nama "ortofoto" di baris 28 boleh diganti, asal rumus di baris 32 ikut diganti.
Jebakan.
- Nama layer dan rumus tidak cocok. Bila baris 28 diganti
"foto"tetapi rumus tetap"ortofoto@1", perhitungan gagal. Bloktry/exceptmenangkap galatnya. Uji menunjukkan log memuat pesanGagal memproses ubin_01.tif: Error occurred while performing calculation., dan tugas berhenti. Pesan "Selesai 100%" tidak muncul dan berkas tidak dibuat. Pesan "❌ Proses dibatalkan atau error." muncul sesudah pesanGagal memproses(tingkat Warning). Tanparesult=Nonepada fungsi penutup, pesan itu tidak pernah tercetak, sebab QGIS memanggil fungsi penutup dengan satu isian pada jalur gagal. Tetap baca pesanGagal memprosesuntuk tahu ubin mana yang gagal. - Jalur dan folder. Sama dengan Skrip 24.4: ganti jalur. Folder keluaran dibuat otomatis oleh baris 15.
Latihan variasi.
- Ganti nama layer menjadi
"foto"di dua tempat: baris 28 dan rumus di baris 32. Jalankan lagi. Hasilnya sama seperti sebelumnya. - Ganti nama di baris 28 saja, lalu jalankan. Baca panel log dan cari pesan yang muncul.
Contoh jawaban 2: log memuat Gagal memproses ubin_01.tif: Error occurred while performing calculation., dan tidak ada berkas hasil baru.
Ekspresi 24.6: teks laporan untuk Atlas (bukan Python) #
Berkas ekspresi_6_3.txt. Berkas ini bukan skrip Python. Isinya sebuah ekspresi QGIS: rumus kecil dengan bahasa tersendiri, yang dipakai di kotak teks atau label. Di buku M1, ekspresi ini ditempel di label tata letak Atlas, yaitu fitur QGIS yang membuat satu halaman peta untuk tiap fitur, misalnya satu halaman per KPH. Bahasa ekspresi tidak mengenal import, print, atau def.
Tujuan. Membuat teks laporan per KPH: nama, luas, dan jumlah titik api aktif.
Bahan. Paket data paket-data: Batas_KPH.gpkg (kolom NAMA_KPH) sebagai layer cakupan Atlas, dan Hotspot_Nasional.gpkg (layer Hotspot_Nasional, 12 titik, kolom ID_Titik). Kedua layer harus ada di proyek. Cara memakainya: tempel di label layout Atlas (bukan di Konsol Python).
Ekspresi utuh:
'LAPORAN KEBAKARAN HUTAN KPH ' || upper("NAMA_KPH") || '\n' ||
'-----------------------------------' || '\n' ||
'Total Luas KPH: ' || round($area / 10000, 2) || ' Ha \n' ||
'Jumlah Titik Api Aktif: ' ||
aggregate(
layer:= 'Hotspot_Nasional',
aggregate:= 'count',
expression:= "ID_Titik",
filter:= intersects($geometry, geometry(@parent))
) || ' Titik.'Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1 | 'LAPORAN KEBAKARAN HUTAN KPH ' ... upper("NAMA_KPH") ... | Dua tanda garis tegak berurutan merangkai teks, dan di berkas tanda itu ada di sela-sela bagian baris ini. upper(...) mengubah nama KPH menjadi huruf besar. Tulisan "NAMA_KPH" dengan kutip ganda berarti kolom. Tulisan '\n' berarti ganti baris. |
| 2 | '-----...' | Garis pemisah, lalu ganti baris. |
| 3 | round($area / 10000, 2) | $area adalah luas fitur yang sedang diproses, dalam meter persegi. Dibagi 10.000 menjadi hektare, lalu dibulatkan dua desimal. |
| 4-10 | aggregate(layer:='Hotspot_Nasional', ...) | Ambil ringkasan dari layer lain. aggregate:= 'count' berarti hitung jumlah, expression:= "ID_Titik" kolom yang dihitung, dan filter:= intersects($geometry, geometry(@parent)) memilih hanya titik yang berada di dalam KPH yang sedang diproses. |
| 10 | ) ... ' Titik.' | Tutup kurung aggregate, lalu sambung dengan kata penutup memakai dua tanda garis tegak. |
Tanda $geometry menunjuk bentuk titik api yang sedang dicek. Tanda @parent menunjuk fitur KPH yang sedang diproses.
Hasil. Ekspresi ini diuji dengan memintanya dihitung untuk tiap KPH di layer Batas_KPH. Hasilnya, tiga laporan:
LAPORAN KEBAKARAN HUTAN KPH KPH ALPHA
-----------------------------------
Total Luas KPH: 4 Ha
Jumlah Titik Api Aktif: 2 Titik.
LAPORAN KEBAKARAN HUTAN KPH KPH BETA
-----------------------------------
Total Luas KPH: 4 Ha
Jumlah Titik Api Aktif: 5 Titik.
LAPORAN KEBAKARAN HUTAN KPH KPH GAMMA
-----------------------------------
Total Luas KPH: 8 Ha
Jumlah Titik Api Aktif: 5 Titik.Kata "KPH" muncul dua kali di judul, sebab ia ada di teks tetap dan di nama KPH. Itu bukan galat.
Ekspresi juga bisa dicoba dari skrip Python. Contoh berikut menghitungnya untuk satu fitur (batas KPH pertama). Alat yang dipakai adalah QgsExpression, yaitu perkakas QGIS yang membaca dan menghitung ekspresi.
from qgis.core import QgsExpression, QgsExpressionContext, QgsExpressionContextUtils, QgsProject
batas = QgsProject.instance().mapLayersByName('Batas_KPH')[0]
teks = open("D:/Latihan/ekspresi_6_3.txt", encoding="utf-8").read()
ekspresi = QgsExpression(teks)
konteks = QgsExpressionContext(QgsExpressionContextUtils.globalProjectLayerScopes(batas))
konteks.setFeature(next(batas.getFeatures()))
print(ekspresi.evaluate(konteks))Layar menampilkan laporan KPH Alpha, empat baris seperti di atas. Dalam uji, ekspresi diparsing tanpa galat.
Bagian yang boleh diubah.
- Baris 1, judul. Ganti teks di antara kutip tunggal.
- Baris 3, satuan. Ubah pembagi
10000untuk satuan lain. Ingat:$areadalam satuan peta, jadi pada data derajat hasilnya bukan hektare. - Baris 6, nama layer. Ganti
'Hotspot_Nasional'dengan layer titik lain. - Baris 8, kolom hitung. Ganti
"ID_Titik"dengan kolom lain dari layer itu.
Jebakan.
- Layer penghitung tidak ada di proyek. Bila
Hotspot_Nasionaltidak dimuat, ekspresi tidak menghasilkan teks. Pada uji lewat skrip, hasilnyaNonedan pesan galatnyaCannot find layer with name or ID 'Hotspot_Nasional'. - Nama layer harus persis. Huruf besar dan kecil dibedakan, dan nama yang dicari adalah nama di panel Layers.
- Tanda ganti baris. Tulisan
\nhanya bermakna di tempat yang mengerti baris baru. Label Atlas menampilkannya sebagai baris baru. Kotak teks sebaris tidak. [CEK]
Latihan variasi. Buat ekspresi baru yang menampilkan kepadatan titik api per hektare. Contoh jawaban: bagi jumlah titik dengan luas dalam hektare, lalu bulatkan.
'Titik api per hektare: ' || round(
aggregate(
layer:= 'Hotspot_Nasional',
aggregate:= 'count',
expression:= "ID_Titik",
filter:= intersects($geometry, geometry(@parent))
) / ($area / 10000), 2)Hasilnya Titik api per hektare: 0.5 untuk KPH Alpha, 1.25 untuk KPH Beta, dan 0.63 untuk KPH Gamma.
Cek paham #
- Mengapa baris 57 Skrip 24.1 membungkus
ndvi_result['OUTPUT']denganQgsRasterLayer? Apa yang terjadi bila baris 54 ditulis'memory:NDVI_KPH'? - Apa bedanya
'TEMPORARY_OUTPUT'dan'memory:Nama'pada hasil raster? - Mengapa Skrip 24.4 sebaiknya dijalankan sebagai tugas latar belakang, dan untuk apa baris 13 serta baris 31-33?
Jawaban.
- Karena alat mengembalikan teks jalur, bukan layer, dan
addMapLayerminta layer. Bila baris 54 ditulis'memory:NDVI_KPH', alat mengembalikan teks'memory:NDVI_KPH', sehinggaaddMapLayergagal denganTypeError. 'TEMPORARY_OUTPUT'membuat berkas sementara dan mengembalikan jalurnya, yang bisa Anda bungkus menjadi layer.'memory:Nama'pada raster hanya mengembalikan teks dan tidak menghasilkan layer.- Agar QGIS tidak membeku saat memproses puluhan ubin. Baris 13 membuat folder keluaran bila belum ada. Baris 31-33 menghentikan tugas bila satu hasil tidak terbentuk. Tanpa keduanya, log bisa menulis "Selesai" padahal tidak ada berkas. Tetap cek isi folder keluaran.
Kesalahan umum #
- Memercayai pesan "Selesai". Pesan itu hanya tulisan di skrip. Perbaikan: cek hasilnya (layer muncul, berkas ada), dan periksa keberadaan berkas di dalam skrip seperti baris 31-33 Skrip 24.4.
- Memakai tulisan memory untuk hasil raster. Perbaikan: pakai
'TEMPORARY_OUTPUT'atau jalur berkas, lalu bungkus denganQgsRasterLayer. - Membiarkan jalur komputer penulis. Perbaikan: ganti baris jalur sesuai komputer Anda dan siapkan folder keluarannya.
- Nama layer di rumus tidak cocok dengan nama layer sebenarnya. Perbaikan: ganti keduanya bersama-sama.
Ringkasan dan latihan #
- Alat Processing dipanggil dengan
processing.run("nama_alat", {kamus})dan mengembalikan kamus hasil. - Hasil alat bisa berupa layer atau teks jalur. Periksa jenisnya sebelum memakainya.
memory:berfungsi untuk vektor, tidak untuk raster. Untuk raster, pakai'TEMPORARY_OUTPUT'atau jalur berkas, lalu bungkus denganQgsRasterLayer(Skrip 24.1 dan 24.2).- Pekerjaan berat dijalankan sebagai tugas latar belakang dengan
QgsTask. Buat folder keluaran, periksa tiap hasil, pantau log, dan cek isi folder. - Ekspresi QGIS (Ekspresi 24.6) adalah bahasa terpisah dari Python.
Latihan. Gabungkan dua skrip: ubah Skrip 24.5 supaya setelah semua ubin selesai, fungsi penutup mencatat jumlah berkas keluaran di folder. Bagian mana yang Anda ubah, dan fungsi apa dari modul os yang membantu?
Tabel perbandingan: tiga cara menulis OUTPUT #
| Isi OUTPUT | Yang dikembalikan | Cocok untuk |
|---|---|---|
'TEMPORARY_OUTPUT' | Teks jalur berkas sementara | Hasil antara, raster maupun vektor |
'memory:Nama' | Layer (vektor) atau teks yang tidak berguna (raster) | Hasil vektor kecil yang langsung dimuat |
Jalur berkas, misalnya 'D:/Hasil/ndvi.tif' | Teks jalur yang sama | Hasil akhir yang harus disimpan |