Lewati ke isi
Profil penulisSeri Buku GIS Kehutanan dan Pertanian/ PY

BAB 21: Memanggil Alat dari Skrip: Satu Perintah, Satu Analisis, Satu Resep untuk Banyak Berkas

Studi kasus: "Sempadan sungai, tiga puluh ubin, dan sore yang pendek" #

Kepala Seksi minta dua hal sebelum akhir pekan. Pertama, luas jalur sempadan sungai 30 meter di tiap KPH, lengkap dengan jumlah titik panasnya. Kedua, NDVI untuk semua ubin foto drone yang baru masuk. Mengklik alat satu per satu akan menghabiskan sore Anda. Dengan skrip, Anda memanggil alat Processing lewat tulisan, merangkainya, mengulanginya untuk banyak berkas, dan membiarkan QGIS tetap hidup selagi bekerja.

Konsep: memanggil alat dengan formulir pesanan #

Di QGIS, Anda memakai alat lewat jendela dengan kolom isian. Di skrip, kolom isian itu ditulis sebagai kamus parameter. Bayangkan formulir pesanan di loket: Anda menyebut nama layanan, mengisi kolomnya, lalu petugas mengembalikan lembar jawaban. Lembar jawaban itu adalah kamus hasil.

Ilustrasi 21.1: Satu panggilan, kamus masuk dan kamus keluar
Skema kamus parameter masuk ke processing.run bersama ID alat, lalu kamus hasil keluar dan diambil dengan kunci OUTPUT

Nama alat di loket itu disebut ID alat, misalnya native:buffer. Bagian sebelum titik dua adalah nama kelompok alat (native berarti alat bawaan QGIS), dan bagian sesudahnya nama alatnya. Kamus sudah dibahas di Bab 5, jadi yang baru di sini hanyalah isinya.

Lima istilah baru Anda:

  • Alat Processing: satu fungsi analisis di QGIS, seperti Buffer, yang juga ada di Processing Toolbox.
  • ID alat: nama unik alat untuk skrip, misalnya native:buffer.
  • Kamus parameter: isian alat, berupa pasangan nama dan nilai.
  • Kamus hasil: jawaban alat, diambil dengan nama keluarannya, misalnya ["OUTPUT"].
  • Keluaran sementara (TEMPORARY_OUTPUT): hasil perantara yang tidak ditulis ke berkas pilihan Anda.

Dua cara menjalankan, dan satu tambahan untuk Processing #

Bab 14 menjelaskan dua cara menjalankan skrip PyQGIS: di dalam QGIS, atau mandiri dengan kerangka QgsApplication. Semua skrip bab ini berjalan dengan cara pertama tanpa tambahan, sebab Konsol Python QGIS sudah memuat qgis.core dan processing.

Untuk cara mandiri, ada tambahan khusus. Python biasa tidak menemukan perkakas processing begitu saja. Pada uji, tulisan import processing di skrip mandiri menghasilkan ModuleNotFoundError: No module named 'processing'. Kerangka berikut sudah diuji pada QGIS 4.0.2.

PYTHONUnduh berkas
import sys
from qgis.core import QgsApplication

QgsApplication.setPrefixPath(r"C:/Program Files/QGIS 4.0.2/apps/qgis", True)
app = QgsApplication([], False)
app.initQgis()

sys.path.append(r"C:/Program Files/QGIS 4.0.2/apps/qgis/python/plugins")
from processing.core.Processing import Processing
Processing.initialize()
from qgis.analysis import QgsNativeAlgorithms
QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())

# ... tempel isi skrip Anda di sini ...

app.exitQgis()
BarisArtinya
sys.path.append(...)Tambahkan folder plugin QGIS ke daftar tempat Python mencari perkakas. Jalur ini milik komputer penulis dan akan berbeda di komputer Anda [CEK].
Processing.initialize()Nyalakan sistem Processing.
addProvider(QgsNativeAlgorithms())Daftarkan alat bawaan (native:) ke buku telepon alat.

Kerangka ini hanya untuk cara mandiri. Bila bagian Processing.initialize() dan addProvider dibuang, skrip tetap bisa mengimpor processing, tetapi alatnya tidak dikenal. Pada uji, hasilnya QgsProcessingException: Error: Algorithm native:buffer not found.

Skrip 21.1: Memanggil satu alat, Buffer #

Tujuan. Mencari ID alat Buffer, mengintip nama-nama isiannya, menjalankannya pada layer sungai dengan jarak 30 meter, lalu membaca hasilnya.

Bahan. Paket data paket-i4, berkas Sungai.gpkg (dua garis sungai). Berkas b2_01_jalankan_alat.py dijalankan dari Editor Skrip QGIS, atau dengan kerangka mandiri di atas. Ganti DATA di baris 6 dengan lokasi paket-i4 Anda.

Skrip ini bekerja dalam enam tahap. Gambar berikut menunjukkan urutannya.

Ilustrasi 21.2: Alur skrip 21.1
Skema enam tahap skrip: muat Sungai, cari ID alat, intip formulir, jalankan Buffer, ambil hasil dan hitung luas, beri nama dan tambahkan ke proyek

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 2.1: Menjalankan satu alat Processing dari Python (Buffer)]
# Penulis: Badar Mubarok Yogaswara
import processing
from qgis.core import QgsProject, QgsVectorLayer, QgsApplication

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"

sungai = QgsVectorLayer(DATA + "/Sungai.gpkg", "Sungai", "ogr")

# 0. Mencari ID alat dengan kata kunci
for a in sorted(QgsApplication.processingRegistry().algorithms(), key=lambda x: x.id()):
    if a.id().startswith("native:") and "buffer" in a.id():
        print("ID:", a.id(), "-", a.displayName())

# 1. Mengintip "formulir" alat: nama parameter dan artinya
alat = QgsApplication.processingRegistry().algorithmById("native:buffer")
print("Alat:", alat.displayName())
for p in alat.parameterDefinitions():
    print("  ", p.name(), "=", p.description())

# 2. Menjalankan alat. Parameter berupa kamus (dictionary): nama parameter -> nilai.
hasil = processing.run("native:buffer", {
    "INPUT": sungai,
    "DISTANCE": 30,                 # satuan meter karena CRS-nya UTM
    "SEGMENTS": 8,
    "END_CAP_STYLE": 0,             # 0 = bulat
    "JOIN_STYLE": 0,                # 0 = bulat
    "MITER_LIMIT": 2,
    "DISSOLVE": True,               # gabungkan semua penyangga jadi satu bentuk
    "OUTPUT": "TEMPORARY_OUTPUT",   # keluaran sementara, tidak menulis berkas
})

# 3. Hasilnya juga kamus. Kuncinya nama keluaran. Untuk vektor sementara, isinya sudah berupa layer.
sempadan = hasil["OUTPUT"]
print("Jenis keluaran:", type(sempadan).__name__)
print("Fitur:", sempadan.featureCount())
luas = sum(f.geometry().area() for f in sempadan.getFeatures()) / 10000
print("Luas sempadan sungai:", round(luas, 2), "ha")

# 4. Beri nama yang jelas lalu tampilkan di proyek
sempadan.setName("Sempadan_30m")
QgsProject.instance().addMapLayer(sempadan)
print("Layer di proyek:", [l.name() for l in QgsProject.instance().mapLayers().values()])

Penjelasan baris demi baris. Dua blok. Blok pertama mengenali alatnya, blok kedua menjalankannya.

Blok A: mengenali alat (baris 3-20).

BarisTulisanArtinya
3-4import processing dan from qgis.core import ...Panggil perkakas Processing dan tiga perkakas QGIS. QgsProject dan QgsVectorLayer dibahas di Bab 14.
6-7DATA dan HASILDua kotak jalur folder. Di skrip ini hanya DATA yang dipakai.
9sungai = QgsVectorLayer(...)Buka layer sungai, seperti di Bab 14.
12-14for a in sorted(...algorithms(), key=lambda x: x.id()):Ambil daftar semua alat, urutkan menurut ID. lambda x: x.id() adalah resep mini yang berbunyi "ambil ID-nya" (dibahas di Bab 7). Untuk tiap alat yang ID-nya diawali native: dan memuat kata buffer, cetak ID dan nama tampilannya.
17-18alat = ...algorithmById("native:buffer")Ambil satu alat dengan ID-nya, lalu cetak nama tampilannya.
19-20for p in alat.parameterDefinitions():Ulangi untuk tiap isian alat. Cetak nama isian (p.name()) dan artinya (p.description()). Inilah "formulir" alat itu.

Blok B: menjalankan dan membaca hasil (baris 22-44).

BarisTulisanArtinya
23-32hasil = processing.run("native:buffer", {...})Jalankan alat. Isian pertama adalah ID alat, isian kedua kamus parameter. Hasilnya disimpan di kotak hasil.
24"INPUT": sungaiLayer masukan.
25"DISTANCE": 30Jarak penyangga. Satuannya mengikuti CRS layer. Di data ini CRS-nya UTM, jadi 30 meter.
26-29"SEGMENTS", "END_CAP_STYLE", "JOIN_STYLE", "MITER_LIMIT"Pengaturan bentuk ujung dan sudut. Angka 0 berarti "bulat" (lihat tabel pilihan di bawah).
30"DISSOLVE": TrueGabungkan semua penyangga menjadi satu bentuk.
31"OUTPUT": "TEMPORARY_OUTPUT"Simpan hasil sebagai keluaran sementara.
35-37sempadan = hasil["OUTPUT"]Ambil jawaban dari kamus hasil dengan kunci OUTPUT. Untuk vektor sementara, isinya sudah berupa layer. Cetak jenisnya dengan type(...).__name__ dan jumlah fitur dengan featureCount().
38luas = sum(f.geometry().area() for f in ...) / 10000Jumlahkan luas semua fitur, lalu ubah meter persegi menjadi hektare.
42-43sempadan.setName(...) lalu addMapLayer(...)Beri nama yang jelas, lalu tampilkan di proyek. Tanpa addMapLayer, layer sementara tidak muncul di panel Layers.
44[l.name() for l in ...mapLayers().values()]Cetak nama semua layer di proyek.

Angka pilihan untuk END_CAP_STYLE dan JOIN_STYLE bisa dilihat dengan processing.algorithmHelp("native:buffer"). Pada uji, bantuan itu mencatat END_CAP_STYLE: 0 Round, 1 Flat, 2 Square. Untuk JOIN_STYLE: 0 Round, 1 Miter, 2 Bevel.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
ID: native:buffer - Buffer
ID: native:bufferbym - Variable width buffer (by M value)
ID: native:multiringconstantbuffer - Multi-ring buffer (constant distance)
ID: native:singlesidedbuffer - Single sided buffer
ID: native:taperedbuffer - Tapered buffers
ID: native:wedgebuffers - Create wedge buffers
Alat: Buffer
   INPUT = Input layer
   DISTANCE = Distance
   SEGMENTS = Segments
   END_CAP_STYLE = End cap style
   JOIN_STYLE = Join style
   MITER_LIMIT = Miter limit
   DISSOLVE = Dissolve result
   SEPARATE_DISJOINT = Keep disjoint results separate
   OUTPUT = Buffered
Jenis keluaran: QgsVectorLayer
Fitur: 1
Luas sempadan sungai: 4.48 ha
Layer di proyek: ['Sempadan_30m']

Ada enam alat yang ID-nya memuat kata "buffer". Alat Buffer punya sembilan isian, dan skrip hanya mengisi delapan. Isian SEPARATE_DISJOINT tidak diisi, jadi alat memakai nilai bawaannya. Hasil: satu fitur seluas 4,48 hektare.

Bagian yang boleh diubah.

  • Baris 6, lokasi data. Ganti sesuai komputer Anda.
  • Baris 13, kata buffer. Ganti dengan kata lain, misalnya clip atau raster, untuk mencari alat lain.
  • Baris 17, ID alat. Ganti dengan ID lain, lalu baca isiannya di blok yang mengintip formulir.
  • Baris 25, jarak. Ganti angka 30. Satuan mengikuti CRS.
  • Baris 30, DISSOLVE. False menghasilkan satu fitur per sungai.
  • Baris 42, nama layer. Ganti sesuai kebutuhan.

Jebakan. Semua kesalahan di bawah sudah dicoba pada QGIS 4.0.2.

  • Nama isian salah ketik tidak menimbulkan galat. "DISTANC": 30 menggantikan "DISTANCE": 30. Skrip jalan mulus, tetapi alat memakai nilai bawaannya sendiri. Luas menjadi 1.44 ha, bukan 4.48 ha. Tidak ada pesan apa pun. Salin nama isian dari hasil blok yang mengintip formulir.
  • Layer masukan gagal dibuka tidak menimbulkan galat. Bila Sungai.gpkg diganti Sungay.gpkg, hasilnya Fitur: 0 dan Luas sempadan sungai: 0.0 ha. Alat bekerja pada layer kosong. Periksa sungai.isValid() lebih dulu (Bab 14).
  • ID alat salah ketik menimbulkan galat. "native:buffr" menghasilkan QgsProcessingException: Error: Algorithm native:buffr not found.
  • Keluaran ke berkas bukan layer. Bila "OUTPUT" diisi jalur berkas, hasil["OUTPUT"] berisi teks jalur itu. Baris 36 mencetak Jenis keluaran: str, lalu baris 37 berhenti dengan AttributeError: 'str' object has no attribute 'featureCount'.
  • Satuan jarak mengikuti CRS. Sungai.gpkg diubah ke EPSG:4326 (derajat), lalu skrip dijalankan. Hasilnya Luas sempadan sungai: 0.28 ha, angka yang tidak bermakna, karena 30 dibaca sebagai 30 derajat. Pastikan CRS berupa meter sebelum memakai DISTANCE.

Latihan variasi.

  1. Jalankan lagi dengan DISSOLVE bernilai False. Berapa fitur dan berapa luasnya?
  2. Mengapa luasnya berbeda dari 4,48 hektare?

Contoh jawaban (sudah diuji). Untuk nomor 1, ubah baris 30 menjadi "DISSOLVE": False,. Layar menampilkan Fitur: 2 dan Luas sempadan sungai: 4.82 ha. Untuk nomor 2, kemungkinan besar bagian yang tumpang tindih di pertemuan dua sungai terhitung dua kali bila tidak digabung. Selisihnya 0,34 hektare [kemungkinan, belum diukur terpisah].

Skrip 21.2: Merangkai alat vektor #

Keluaran satu alat bisa langsung menjadi masukan alat berikutnya, seperti ban berjalan di pabrik. Skrip ini merangkai tiga alat, lalu menggabungkan hasilnya dalam satu tabel kecil.

Tujuan. Menghitung luas sempadan sungai 30 meter di tiap KPH, persennya terhadap luas KPH, dan jumlah titik panas per KPH.

Bahan. Paket data paket-i4: Batas_KPH.gpkg, Sungai.gpkg, dan Hotspot_Nasional.gpkg. Berkas b2_02_rantai_vektor.py. Ganti DATA di baris 6.

Ilustrasi 21.3: Alur skrip 21.2
Skema enam tahap skrip: muat tiga layer, buffer sungai, irisan dengan KPH, hitung titik panas, jumlahkan luas per KPH, cetak tabel

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 2.2: Rantai alat vektor: sempadan sungai dan titik panas per KPH]
# Penulis: Badar Mubarok Yogaswara
import processing
from qgis.core import QgsProject, QgsVectorLayer

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"

batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
sungai = QgsVectorLayer(DATA + "/Sungai.gpkg", "Sungai", "ogr")
hotspot = QgsVectorLayer(DATA + "/Hotspot_Nasional.gpkg", "Hotspot_Nasional", "ogr")

# Langkah 1: penyangga (buffer) 30 m di kiri-kanan sungai
sempadan = processing.run("native:buffer", {
    "INPUT": sungai, "DISTANCE": 30, "SEGMENTS": 8, "END_CAP_STYLE": 0, "JOIN_STYLE": 0,
    "MITER_LIMIT": 2, "DISSOLVE": True, "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]

# Langkah 2: irisan (intersection). Hasil langkah 1 langsung menjadi masukan langkah 2.
irisan = processing.run("native:intersection", {
    "INPUT": batas, "OVERLAY": sempadan, "INPUT_FIELDS": ["NAMA_KPH"],
    "OVERLAY_FIELDS": [], "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]

# Langkah 3: hitung titik panas di tiap KPH
dengan_titik = processing.run("native:countpointsinpolygon", {
    "POLYGONS": batas, "POINTS": hotspot, "FIELD": "JML_HOTSPOT", "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]

# Langkah 4: gabungkan angkanya dalam satu tabel kecil
luas_sempadan = {}
for f in irisan.getFeatures():
    nama = f["NAMA_KPH"]
    luas_sempadan[nama] = luas_sempadan.get(nama, 0) + f.geometry().area() / 10000

print("KPH         luas(ha)  sempadan(ha)  persen  titik_panas")
for f in dengan_titik.getFeatures():
    nama = f["NAMA_KPH"]
    luas = f.geometry().area() / 10000
    sem = luas_sempadan.get(nama, 0)
    print("%-11s %8.2f %13.2f %7.1f %12d" % (nama, luas, sem, 100 * sem / luas, f["JML_HOTSPOT"]))

Skrip ini mirip Skrip 21.1, jadi hanya bagian yang baru yang dijelaskan. Baris import, DATA, dan pemuatan layer (baris 3-11) sudah dibahas sebelumnya.

BarisTulisanArtinya
14-16sempadan = processing.run("native:buffer", {...})["OUTPUT"]Sama dengan Skrip 21.1, tetapi ["OUTPUT"] ditulis langsung di ujung. Kotak sempadan langsung berisi layer hasil.
19-21irisan = processing.run("native:intersection", {...})Iris layer batas KPH dengan sempadan. INPUT adalah batas, OVERLAY adalah sempadan. Hasil langkah 1 menjadi masukan langkah 2.
20"INPUT_FIELDS": ["NAMA_KPH"]Kolom dari INPUT yang dibawa ke hasil. Daftar kosong ([]) pada OVERLAY_FIELDS berarti, menurut keterangan alat, semua kolom dibawa.
24-25dengan_titik = processing.run("native:countpointsinpolygon", {...})Hitung titik panas di dalam tiap KPH. POLYGONS adalah batas, POINTS adalah titik panas, dan FIELD adalah nama kolom penghitung baru, JML_HOTSPOT.
28luas_sempadan = {}Siapkan kamus kosong.
29-31for f in irisan.getFeatures():Untuk tiap irisan: ambil nama KPH, lalu tambahkan luasnya (dalam hektare) ke jumlah milik KPH itu. luas_sempadan.get(nama, 0) berarti "ambil jumlah sejauh ini, atau 0 bila belum ada".
33print("KPH luas(ha) ...")Cetak judul tabel. Spasinya diketik tangan agar sejajar dengan baris 38.
34-37for f in dengan_titik.getFeatures():Untuk tiap KPH: ambil nama, luas, dan luas sempadannya (.get(nama, 0)).
38print("%-11s %8.2f %13.2f %7.1f %12d" % (...))Cetak satu baris tabel. %-11s teks rata kiri selebar 11 karakter, %8.2f angka dua desimal selebar 8, %d bilangan bulat. Persen dihitung dengan 100 * sem / luas. Format % dibahas di Bab 9.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
KPH         luas(ha)  sempadan(ha)  persen  titik_panas
KPH Alpha       4.00          1.18    29.4            2
KPH Beta        4.00          1.39    34.6            5
KPH Gamma       8.00          1.50    18.7            5

Dari tabel ini Anda tahu: sempadan paling luas di KPH Gamma (1,50 hektare), tetapi persen terbesar ada di KPH Beta (34,6 persen).

Bagian yang boleh diubah.

  • Baris 15, "DISTANCE": 30. Lebar sempadan.
  • Baris 25, "FIELD": "JML_HOTSPOT". Nama kolom penghitung. Bila Anda mengubahnya, ubah juga f["JML_HOTSPOT"] di baris 38.
  • Baris 20, INPUT_FIELDS. Tambahkan nama kolom lain yang ingin dibawa.
  • Baris 33 dan 38, format tabel. Lebar kolom boleh disesuaikan, tetapi judul dan baris data harus selaras.

Jebakan. Semua dicoba pada QGIS 4.0.2.

  • Nama kolom tidak sama. f["JML_HOTSPOT"] diganti f["JML_TITIK"]: judul tabel tercetak, lalu skrip berhenti dengan KeyError: 'JML_TITIK'.
  • Layer sungai gagal dibuka. Dengan Sungay.gpkg, tidak ada galat. Kolom sempadan berisi 0.00 dan persen 0.0 untuk ketiga KPH. Angka tampak wajar, padahal kosong.
  • Jarak nol. "DISTANCE": 0 menghentikan alat Intersection dengan QgsProcessingException: Could not write feature into OUTPUT: Could not add feature with geometry type GeometryCollection to layer of type MultiPolygon. Jarak nol menghasilkan bentuk yang tidak cocok untuk alat berikutnya.
  • Pembagian dengan nol. Baris 38 membagi dengan luas. Data latihan tidak punya KPH berluas nol, jadi galat ZeroDivisionError tidak muncul di sini (Bab 10 membahas penangannya).

Latihan variasi. Ubah lebar sempadan menjadi 50 meter. Bagaimana tabelnya berubah?

Contoh jawaban (sudah diuji). Ganti baris 15 menjadi "INPUT": sungai, "DISTANCE": 50, "SEGMENTS": 8, "END_CAP_STYLE": 0, "JOIN_STYLE": 0,. Layar menampilkan:

HASIL
KPH         luas(ha)  sempadan(ha)  persen  titik_panas
KPH Alpha       4.00          1.69    42.2            2
KPH Beta        4.00          2.32    58.0            5
KPH Gamma       8.00          2.54    31.8            5

Skrip 21.3: Alat raster, NDVI dan tinggi tajuk #

Raster adalah kisi petak (piksel), jadi alatnya sedikit berbeda dari vektor. Skrip ini memakai dua alat raster dan satu alat statistik. Hasilnya dua angka per KPH.

Dua istilah baru. NDVI adalah indeks kehijauan tanaman dari citra, dihitung (NIR - Merah) / (NIR + Merah). Nilai tinggi berarti vegetasi rapat. CHM adalah tinggi tajuk pohon, dihitung dari raster tinggi permukaan (DSM) dikurangi raster tinggi tanah (DTM). Statistik zonal meringkas nilai raster di dalam tiap poligon.

Tujuan. Menghitung NDVI dan tinggi tajuk, lalu mencetak rata-ratanya untuk tiap KPH.

Bahan. Paket data paket-i4: Batas_KPH.gpkg, Citra_KPH.tif (empat band, band 1 Merah dan band 4 NIR), DSM_Drone.tif, dan DTM_Drone.tif. Berkas b2_03_raster_alat.py. Ganti DATA di baris 6.

Ilustrasi 21.4: Alur skrip 21.3
Skema enam tahap skrip: muat batas dan raster, hitung NDVI dengan rastercalc, hitung CHM, fungsi rata_rata, statistik zonal, cetak dan tambahkan layer

Hal yang perlu Anda tahu dulu: keluaran raster sementara berbeda dari vektor sementara.

Ilustrasi 21.5: Isi hasil OUTPUT bergantung pada jenisnya
Skema tiga kasus: vektor sementara berisi layer, raster sementara berisi teks jalur, dan keluaran berupa jalur berkas juga berisi teks jalur

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 2.3: Alat raster dari Python: NDVI, tinggi tajuk (CHM), dan statistik per KPH]
# Penulis: Badar Mubarok Yogaswara
import processing
from qgis.core import QgsProject, QgsVectorLayer, QgsRasterLayer

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"

batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
citra = QgsRasterLayer(DATA + "/Citra_KPH.tif", "Citra_KPH", "gdal")   # band 1=Merah, 4=NIR
dsm = QgsRasterLayer(DATA + "/DSM_Drone.tif", "DSM", "gdal")
dtm = QgsRasterLayer(DATA + "/DTM_Drone.tif", "DTM", "gdal")

# 1. NDVI = (NIR - Merah) / (NIR + Merah). Dalam ekspresi, band ditulis "NamaLayer@nomorBand".
jalur_ndvi = processing.run("native:rastercalc", {
    "EXPRESSION": '("Citra_KPH@4" - "Citra_KPH@1") / ("Citra_KPH@4" + "Citra_KPH@1")',
    "LAYERS": [citra], "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
print("Keluaran raster sementara berupa:", type(jalur_ndvi).__name__)    # teks jalur, bukan layer
ndvi = QgsRasterLayer(jalur_ndvi, "NDVI_KPH", "gdal")                    # bungkus jadi layer
print("NDVI sah?", ndvi.isValid())

# 2. CHM = DSM - DTM
jalur_chm = processing.run("native:rastercalc", {
    "EXPRESSION": '"DSM@1" - "DTM@1"', "LAYERS": [dsm, dtm], "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
chm = QgsRasterLayer(jalur_chm, "CHM", "gdal")

# 3. Statistik zonal: rata-rata nilai raster di dalam tiap poligon KPH
def rata_rata(raster, awalan):
    return processing.run("native:zonalstatisticsfb", {
        "INPUT": batas, "INPUT_RASTER": raster, "RASTER_BAND": 1,
        "COLUMN_PREFIX": awalan, "STATISTICS": [2],      # 2 = Mean
        "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]

zn = rata_rata(ndvi, "ndvi_")
zc = rata_rata(chm, "chm_")
chm_per_kph = {f["NAMA_KPH"]: f["chm_mean"] for f in zc.getFeatures()}
print("KPH         NDVI rata-rata   CHM rata-rata (m)")
for f in zn.getFeatures():
    print("%-11s %12.3f %16.2f" % (f["NAMA_KPH"], f["ndvi_mean"], chm_per_kph[f["NAMA_KPH"]]))

QgsProject.instance().addMapLayer(ndvi)
QgsProject.instance().addMapLayer(chm)

Penjelasan baris demi baris. Hanya yang baru yang dijelaskan.

BarisTulisanArtinya
9-12batas, citra, dsm, dtmBuka satu layer vektor dan tiga raster. Raster memakai QgsRasterLayer(..., "gdal"). Nama kedua ("Citra_KPH", "DSM", "DTM") penting, karena dipakai di ekspresi.
15-17jalur_ndvi = processing.run("native:rastercalc", {...})["OUTPUT"]Jalankan kalkulator raster. EXPRESSION berisi rumus NDVI. Band ditulis "NamaLayer@nomorBand", misalnya "Citra_KPH@4" untuk band 4. LAYERS berisi daftar layer yang namanya dipakai di rumus.
18print(type(jalur_ndvi).__name__)Cetak jenis hasil. Jawabannya str: teks jalur, bukan layer.
19ndvi = QgsRasterLayer(jalur_ndvi, "NDVI_KPH", "gdal")Bungkus jalur itu menjadi layer raster.
20print("NDVI sah?", ndvi.isValid())Buktikan pembungkusnya berhasil.
23-25jalur_chm = ... dan chm = ...Hal yang sama untuk CHM, dengan rumus "DSM@1" - "DTM@1".
28-32def rata_rata(raster, awalan):Resep bernama yang menjalankan alat statistik zonal. Dibahas di Bab 7. Ia menerima raster dan awalan nama kolom, lalu mengembalikan layer batas dengan kolom baru.
29-31"native:zonalstatisticsfb"Alat statistik zonal. INPUT poligon, INPUT_RASTER raster, RASTER_BAND nomor band, COLUMN_PREFIX awalan nama kolom hasil, dan STATISTICS daftar statistik. Angka 2 berarti rata-rata (Mean).
34-35zn = rata_rata(ndvi, "ndvi_") dan zc = rata_rata(chm, "chm_")Panggil resep dua kali. Kolom hasil bernama awalan ditambah mean: ndvi_mean dan chm_mean.
36chm_per_kph = {f["NAMA_KPH"]: f["chm_mean"] for f in zc.getFeatures()}Susun kamus: nama KPH sebagai kunci, rata-rata CHM sebagai nilai. Cara menulis ini disebut dictionary comprehension.
37-39for f in zn.getFeatures():Cetak tabel: nama KPH, ndvi_mean, dan CHM dari kamus tadi.
41-42addMapLayer(ndvi) dan addMapLayer(chm)Tampilkan dua raster di proyek.

Pada uji, bantuan alat native:zonalstatisticsfb mencatat kode statistik: 0 Count, 1 Sum, 2 Mean, 3 Median, 4 St dev, 5 Minimum, 6 Maximum, dan seterusnya.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
Keluaran raster sementara berupa: str
NDVI sah? True
KPH         NDVI rata-rata   CHM rata-rata (m)
KPH Alpha          0.604             2.60
KPH Beta           0.431             2.69
KPH Gamma          0.329             2.56

Dari hasil ini Anda tahu: vegetasi paling hijau ada di KPH Alpha (NDVI 0,604), sedangkan tajuk paling tinggi rata-rata di KPH Beta (2,69 meter).

Bagian yang boleh diubah.

  • Baris 16, rumus NDVI. Nomor band harus sesuai urutan band citra Anda. Citra ini: band 1 Merah, band 4 NIR.
  • Baris 31, "STATISTICS": [2]. Ganti kode statistik. Perhatikan: resep rata_rata dipakai untuk NDVI dan CHM sekaligus, jadi perubahannya berlaku untuk keduanya.
  • Baris 34-36, awalan kolom. Bila Anda mengubah awalan, ubah juga nama kolom yang dibaca.
  • Baris 24, rumus CHM. Ganti raster atau rumusnya.

Jebakan. Semua dicoba pada QGIS 4.0.2.

  • Band tertukar, tanpa galat. Rumus NDVI ditulis terbalik: "Citra_KPH@1" di tempat "Citra_KPH@4" dan sebaliknya. Skrip jalan mulus, tetapi tabel menampilkan -0.604, -0.431, dan -0.329. Tanda minus adalah petunjuk bahwa band terbalik.
  • Nama layer di rumus tidak cocok. "DSM@1" diganti "DSM_Drone@1", padahal layer bernama "DSM". Hasilnya QgsProcessingException: Error occurred while performing calculation.
  • Nama kolom hasil salah. Menulis f["chm_rata"] menghasilkan KeyError: 'chm_rata'. Kolom hasil selalu awalan ditambah nama statistik (mean, max, dan seterusnya).
  • Hasil raster adalah teks. Memanggil jalur_ndvi.width() gagal, karena str tidak punya metode itu. Bungkus dulu dengan QgsRasterLayer.

Latihan variasi. Ganti kolom CHM menjadi nilai tertinggi per KPH, bukan rata-rata.

Contoh jawaban (sudah diuji). Ubah tiga hal: pada baris 31 jadi "STATISTICS": [2, 6] (rata-rata dan tertinggi sekaligus), pada baris 36 jadi f["chm_max"], dan pada baris 37 ganti judul menjadi CHM tertinggi (m). Layar menampilkan:

HASIL
KPH         NDVI rata-rata   CHM tertinggi (m)
KPH Alpha          0.604            13.94
KPH Beta           0.431            13.96
KPH Gamma          0.329            13.96

Konsep: batch, satu resep untuk banyak berkas #

Tiga skrip berikutnya membahas batch: pekerjaan yang sama diulang untuk banyak masukan. Bayangkan Anda memasak resep yang sama untuk tiga puluh porsi. Resepnya ditulis sekali, lalu perulangan for (Bab 6) memasaknya satu porsi demi satu porsi. Buku I4 Bab 3 menunjukkan cara bawaan QGIS (Execute as batch process). Cara itu cocok untuk beberapa berkas. Untuk puluhan berkas, skrip lebih ringkas.

Ilustrasi 21.6: Satu resep, banyak berkas
Skema tiga ubin masuk ke satu perulangan for dan keluar sebagai tiga berkas NDVI

Istilah baru bagian ini:

  • Batch: pekerjaan sama, banyak masukan.
  • Pola berkas (glob): cara mencari banyak berkas sekaligus, misalnya semua yang berakhiran .tif.
  • Latar belakang: jalur kerja terpisah, supaya QGIS tidak membeku.

Skrip 21.4: Batch sederhana, NDVI untuk semua ubin #

Tujuan. Menghitung NDVI untuk semua ubin ortofoto di satu folder dengan satu perulangan for, menyimpan tiap hasil sebagai berkas, dan mencetak rata-ratanya.

Bahan. Paket data paket-i4, folder Ortofoto_Mentah (tiga ubin; pada data ini band 1 adalah NIR dan band 2 adalah Merah). Berkas b3_01_batch_ndvi.py. Ganti DATA di baris 8 dan HASIL di baris 9. Skrip membuat folder NDVI_ubin di dalam HASIL.

Ilustrasi 21.7: Alur skrip 21.4
Skema enam tahap skrip: siapkan folder, kumpulkan berkas, ulangi tiap ubin, hitung NDVI ke berkas, hitung rata-rata NDVI, cetak ringkasan

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 3.1: Batch sederhana: NDVI untuk semua ubin ortofoto dalam satu folder]
# Penulis: Badar Mubarok Yogaswara
import glob
import os
import processing
from qgis.core import QgsRasterLayer

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"
folder_masuk = DATA + "/Ortofoto_Mentah"
folder_keluar = HASIL + "/NDVI_ubin"
os.makedirs(folder_keluar, exist_ok=True)       # buat folder bila belum ada

# 1. Kumpulkan semua berkas .tif, diurutkan agar hasilnya selalu berurutan sama
daftar = sorted(glob.glob(folder_masuk + "/*.tif"))
print("Ditemukan", len(daftar), "ubin")

# 2. Ulangi pekerjaan yang sama untuk tiap berkas. Pada ubin ini band 1 = NIR dan band 2 = Merah.
berhasil = []
for jalur in daftar:
    nama = os.path.basename(jalur)                         # contoh: ubin_01.tif
    keluar = folder_keluar + "/NDVI_" + nama
    ubin = QgsRasterLayer(jalur, "ubin", "gdal")           # nama "ubin" dipakai di ekspresi
    processing.run("native:rastercalc", {
        "EXPRESSION": '("ubin@1" - "ubin@2") / ("ubin@1" + "ubin@2")',
        "LAYERS": [ubin], "OUTPUT": keluar})
    hasil = QgsRasterLayer(keluar, "hasil", "gdal")
    rata = processing.run("native:rasterlayerstatistics", {"INPUT": hasil, "BAND": 1})["MEAN"]
    print("  %s -> %s  | NDVI rata-rata %.3f" % (nama, os.path.basename(keluar), rata))
    berhasil.append(keluar)

print("Selesai:", len(berhasil), "dari", len(daftar), "berkas")

Penjelasan baris demi baris.

BarisTulisanArtinya
3-6import glob, import os, import processing, from qgis.core ...glob untuk mencari berkas, os untuk urusan folder dan nama berkas.
10-12folder_masuk, folder_keluar, os.makedirs(...)Tentukan folder masukan dan keluaran, lalu buat folder keluaran bila belum ada.
15daftar = sorted(glob.glob(folder_masuk + "/*.tif"))glob.glob mengembalikan semua jalur berkas berakhiran .tif. sorted mengurutkannya agar hasil selalu berurutan sama.
16print("Ditemukan", len(daftar), "ubin")Cetak jumlah berkas yang ditemukan.
19berhasil = []Daftar kosong untuk mencatat hasil.
20for jalur in daftar:Mulai perulangan. Di tiap putaran, jalur berisi satu berkas.
21-22nama = os.path.basename(jalur) lalu keluar = ...Ambil nama berkas saja (misalnya ubin_01.tif), lalu susun jalur keluaran NDVI_ubin_01.tif.
23ubin = QgsRasterLayer(jalur, "ubin", "gdal")Buka ubin sebagai layer bernama ubin. Nama ini dipakai di rumus.
24-26processing.run("native:rastercalc", {...})Hitung NDVI. Band 1 adalah NIR dan band 2 adalah Merah, jadi rumusnya ("ubin@1" - "ubin@2") / ("ubin@1" + "ubin@2"). OUTPUT berisi jalur berkas, jadi hasilnya ditulis ke sana.
27hasil = QgsRasterLayer(keluar, "hasil", "gdal")Buka berkas hasil sebagai layer.
28rata = processing.run("native:rasterlayerstatistics", {...})["MEAN"]Jalankan alat statistik raster. Hasilnya kamus dengan beberapa kunci. Skrip mengambil kunci MEAN.
29-30print(...) dan berhasil.append(keluar)Cetak satu baris ringkas, lalu catat berkas ke daftar.
32print("Selesai:", len(berhasil), "dari", len(daftar), "berkas")Cetak ringkasan akhir.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
Ditemukan 3 ubin
  ubin_01.tif -> NDVI_ubin_01.tif  | NDVI rata-rata 0.471
  ubin_02.tif -> NDVI_ubin_02.tif  | NDVI rata-rata 0.471
  ubin_03.tif -> NDVI_ubin_03.tif  | NDVI rata-rata 0.467
Selesai: 3 dari 3 berkas

Di folder NDVI_ubin kini ada tiga berkas NDVI. QGIS dapat menambahkan berkas pendamping berakhiran .aux.xml. Berkas itu aman dibiarkan.

Bagian yang boleh diubah.

  • Baris 10, folder masukan. Ganti /Ortofoto_Mentah dengan folder lain.
  • Baris 15, pola "/*.tif". Ganti ke "/ubin_0*.tif" untuk memilih sebagian berkas.
  • Baris 25, rumus. Urutan band harus sesuai data Anda. Perhatikan: di Citra_KPH.tif (Skrip 21.3), band 4 adalah NIR, sedangkan di ubin ini band 1.
  • Baris 22, awalan "NDVI_". Ganti sesuai selera.

Jebakan. Semua dicoba pada QGIS 4.0.2.

  • Folder salah, tanpa galat. Ortofoto_Mentah diganti Ortofoto_Mentahh. Layar menampilkan Ditemukan 0 ubin dan Selesai: 0 dari 0 berkas. Perulangan tidak berputar sama sekali. Baca baris pertama hasil.
  • Satu berkas rusak menghentikan semuanya. Folder Ortofoto_Campur berisi empat ubin, dan ubin keempat rusak. Layar menampilkan Ditemukan 4 ubin, tiga baris hasil, lalu ERROR 1: Attempt to create -2147483648x-2147483648 dataset is illegal,sizes must be larger than zero. dan berakhir dengan QgsProcessingException: Error creating output file. Tiga ubin pertama sudah selesai, tetapi ringkasan akhir tidak pernah tercetak. Penanganannya (try dan except) dibahas di Bab 10.
  • Band terbalik, tanpa galat. Rumus ditulis ("ubin@2" - "ubin@1") / .... Hasilnya -0.471, -0.471, dan -0.467. Tanda minus menandakan band terbalik.
  • Band yang tidak ada. Menulis "ubin@4" pada ubin dua band menghasilkan QgsProcessingException: Error occurred while performing calculation.
  • berhasil selalu sama panjang. Daftar berhasil selalu terisi setiap putaran, sehingga ringkasan "3 dari 3" belum berarti ada penyaringan. Ia baru berguna bila disertai penanganan galat.

Latihan variasi. Tambahkan satu baris di dalam perulangan yang mencetak ukuran piksel tiap hasil.

Contoh jawaban (sudah diuji). Sisipkan sebelum berhasil.append(keluar):

PYTHON
    print("     ukuran:", hasil.width(), "x", hasil.height(), "piksel")

Di bawah tiap baris NDVI, layar menampilkan ukuran: 100 x 100 piksel.

Skrip 21.5: Satu putaran untuk tiap fitur #

Perulangan tidak harus atas berkas. Skrip ini berputar atas fitur: untuk tiap KPH, ia memilih satu poligon lalu memotong raster tinggi tajuk (CHM) dengannya. Hasilnya satu raster per KPH.

Tujuan. Menghasilkan raster CHM terpisah untuk tiap KPH, lalu mencetak ukuran, rata-rata, dan nilai tertingginya.

Bahan. Paket data paket-i4: Batas_KPH.gpkg, DSM_Drone.tif, dan DTM_Drone.tif. Berkas b3_02_batch_per_kph.py. Ganti DATA di baris 7 dan HASIL di baris 8. Skrip membuat folder CHM_per_KPH.

Ilustrasi 21.8: Alur skrip 21.5
Skema enam tahap skrip: siapkan folder, muat batas DSM dan DTM, hitung CHM satu kali, pilih satu KPH, potong CHM dengan KPH itu, hitung statistik dan cetak

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 3.2: Batch per fitur: potong raster tinggi tajuk (CHM) untuk tiap KPH]
# Penulis: Badar Mubarok Yogaswara
import os
import processing
from qgis.core import QgsVectorLayer, QgsRasterLayer

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"
folder_keluar = HASIL + "/CHM_per_KPH"
os.makedirs(folder_keluar, exist_ok=True)

batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
dsm = QgsRasterLayer(DATA + "/DSM_Drone.tif", "DSM", "gdal")
dtm = QgsRasterLayer(DATA + "/DTM_Drone.tif", "DTM", "gdal")

# 1. CHM untuk seluruh area (satu kali saja, di luar pengulangan)
chm_jalur = processing.run("native:rastercalc", {
    "EXPRESSION": '"DSM@1" - "DTM@1"', "LAYERS": [dsm, dtm], "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]

# 2. Satu putaran untuk tiap KPH: pilih satu poligon, lalu potong CHM dengannya
for fitur in batas.getFeatures():
    nama = fitur["NAMA_KPH"]                                 # contoh: KPH Alpha
    aman = nama.replace(" ", "_")                            # untuk nama berkas: KPH_Alpha
    satu_kph = processing.run("native:extractbyexpression", {
        "INPUT": batas, "EXPRESSION": '"NAMA_KPH" = \'%s\'' % nama,
        "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
    keluar = folder_keluar + "/CHM_" + aman + ".tif"
    processing.run("gdal:cliprasterbymasklayer", {
        "INPUT": chm_jalur, "MASK": satu_kph, "CROP_TO_CUTLINE": True,
        "NODATA": -9999, "OUTPUT": keluar})
    hasil = QgsRasterLayer(keluar, aman, "gdal")
    st = processing.run("native:rasterlayerstatistics", {"INPUT": hasil, "BAND": 1})
    print("%-10s %4d x %4d piksel | rata-rata %.2f m | tertinggi %.2f m" % (
        aman, hasil.width(), hasil.height(), st["MEAN"], st["MAX"]))
print("Berkas di folder:", sorted(os.listdir(folder_keluar)))

Penjelasan baris demi baris. Hanya yang baru yang dijelaskan.

BarisTulisanArtinya
17-18chm_jalur = processing.run("native:rastercalc", {...})["OUTPUT"]Hitung CHM untuk seluruh area satu kali saja, di luar perulangan. Menghitung ulang di tiap putaran hanya membuang waktu.
21for fitur in batas.getFeatures():Satu putaran untuk tiap KPH.
22-23nama = fitur["NAMA_KPH"] lalu aman = nama.replace(" ", "_")Ambil nama KPH, lalu ganti spasi dengan garis bawah untuk nama berkas: KPH_Alpha.
24-26satu_kph = processing.run("native:extractbyexpression", {...})["OUTPUT"]Ambil satu poligon saja dengan ekspresi. '"NAMA_KPH" = \'%s\'' % nama menyisipkan nama ke dalam teks ekspresi, dengan kutip tunggal di sekelilingnya.
27keluar = folder_keluar + "/CHM_" + aman + ".tif"Susun nama berkas keluaran.
28-30processing.run("gdal:cliprasterbymasklayer", {...})Potong CHM dengan poligon tadi. INPUT raster yang dipotong, MASK poligon pemotong, CROP_TO_CUTLINE berarti ukuran hasil mengikuti poligon, dan NODATA nilai untuk piksel di luar poligon.
31-32hasil = QgsRasterLayer(...) lalu st = ...rasterlayerstatistics...Buka hasil, lalu hitung statistiknya. Kali ini seluruh kamus disimpan di st, bukan hanya satu kuncinya.
33-34print("%-10s %4d x %4d piksel ..." % (...))Cetak satu baris: nama, lebar, tinggi, rata-rata (st["MEAN"]), dan nilai tertinggi (st["MAX"]). Tanda garis tegak di dalam tulisan yang dicetak hanyalah pemisah tampilan.
35print("Berkas di folder:", sorted(os.listdir(folder_keluar)))Cetak isi folder keluaran, diurutkan.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
KPH_Alpha   400 x  400 piksel | rata-rata 2.60 m | tertinggi 13.94 m
KPH_Beta    400 x  400 piksel | rata-rata 2.69 m | tertinggi 13.96 m
KPH_Gamma   800 x  400 piksel | rata-rata 2.56 m | tertinggi 13.96 m
Berkas di folder: ['CHM_KPH_Alpha.tif', 'CHM_KPH_Alpha.tif.aux.xml', 'CHM_KPH_Beta.tif', 'CHM_KPH_Beta.tif.aux.xml', 'CHM_KPH_Gamma.tif']

Perhatikan daftar berkas di baris terakhir. Berkas .aux.xml dibuat oleh QGIS saat menghitung statistik, bukan oleh skrip secara sengaja. Isi daftar di komputer Anda bisa berbeda, misalnya CHM_KPH_Gamma.tif.aux.xml ikut muncul bila skrip dijalankan untuk kedua kalinya. KPH Gamma berukuran 800 kali 400 piksel karena areanya dua kali lebih luas.

Bagian yang boleh diubah.

  • Baris 9-10, folder keluaran. Ganti nama folder.
  • Baris 25, kolom NAMA_KPH. Ganti bila data Anda memakai kolom lain.
  • Baris 29, CROP_TO_CUTLINE. True memotong ukuran hasil mengikuti KPH.
  • Baris 30, NODATA. Nilai -9999 menandai piksel kosong. Nilai ini ikut dikenali saat statistik dihitung.
  • Baris 17-18, rumus CHM. Ganti rumus atau raster, misalnya untuk NDVI.

Jebakan. Semua dicoba pada QGIS 4.0.2.

  • Nama KPH yang memuat kutip tunggal. Pada uji, nama KPH D'Alpha disisipkan ke baris 25. Alat berhenti dengan Incomplete expression. You might not have finished the full expression. Kutip tunggal di tengah nama mengakhiri teks ekspresi lebih awal. Data latihan tidak memuat nama seperti itu.
  • Nama kolom salah. fitur["NAMA_KPH"] diganti fitur["NAMA"]: KeyError: 'NAMA'.
  • CROP_TO_CUTLINE bernilai False. Tidak ada galat. Ketiga hasil berukuran 800 x 800 piksel, karena ukuran tidak dipotong mengikuti KPH. Rata-rata Beta dan Gamma di layar tetap 2.69 dan 2.56.
  • Berkas pendamping di daftar. Karena baris 35 mencetak semua isi folder, berkas .aux.xml ikut tampil.

Latihan variasi. Jalankan hanya untuk KPH Gamma. Bagian mana yang cukup ditambah?

Contoh jawaban (sudah diuji). Sisipkan dua baris setelah baris 22:

PYTHON
    if nama != "KPH Gamma":
        continue

continue berarti "lewati sisa putaran ini, lanjut ke fitur berikutnya". Layar menampilkan satu baris KPH_Gamma 800 x 400 piksel | rata-rata 2.56 m | tertinggi 13.96 m, disusul daftar isi folder (yang juga memuat berkas dari uji sebelumnya).

Konsep: latar belakang dan fungsi selesai #

Skrip 21.4 dan 21.5 membuat QGIS terasa macet selama bekerja. Bayangkan Anda pelayan restoran yang juga harus memasak. Selama Anda memasak, tamu tidak terlayani. Solusinya: serahkan masakan ke juru masak di dapur belakang, sementara Anda tetap melayani. QGIS menyediakan juru masak ini sebagai QgsTask, yaitu tugas latar belakang.

Ilustrasi 21.9: Program utama dan latar belakang
Skema dua jalur: program utama mengirim tugas lalu tetap dipakai, latar belakang menghitung tiap ubin lalu mengembalikan daftar, dan fungsi selesai berjalan di program utama setelah tugas berakhir

Pembagian kerjanya begini. Fungsi tugas berjalan di dapur belakang dan hanya boleh memasak (menghitung). Fungsi selesai berjalan di depan setelah masakan jadi, dan aman untuk menyentuh proyek, misalnya menambah layer. Fungsi selesai disebut juga callback, yaitu fungsi yang dipanggil kembali setelah tugas berakhir.

Tiga istilah baru:

  • QgsTask: tugas latar belakang milik QGIS.
  • Fungsi tugas: fungsi yang dikerjakan di latar belakang.
  • Fungsi selesai: fungsi yang dipanggil di program utama setelah tugas berakhir.

Skrip 21.6: Batch di latar belakang dengan QgsTask #

Tujuan. Menjalankan batch NDVI di latar belakang, lengkap dengan bilah kemajuan dan kemungkinan pembatalan, lalu menambahkan hasilnya ke proyek.

Bahan. Paket data paket-i4, folder Ortofoto_Mentah. Berkas b3_03_batch_latar_belakang.py. Ganti DATA di baris 8 dan HASIL di baris 9. Skrip membuat folder NDVI_latar. Skrip ini paling pas dijalankan di Editor Skrip QGIS yang terbuka, karena tugas latar belakang butuh QGIS yang hidup.

Ilustrasi 21.10: Alur skrip 21.6
Skema enam tahap skrip: siapkan folder, definisikan fungsi tugas, definisikan fungsi selesai, buat tugas, kirim ke manajer tugas, cetak pesan

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 3.3: Batch di latar belakang dengan QgsTask supaya QGIS tidak macet]
# Penulis: Badar Mubarok Yogaswara
import glob
import os
import processing
from qgis.core import QgsApplication, QgsTask, QgsMessageLog, Qgis, QgsRasterLayer, QgsProject

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"
folder_masuk = DATA + "/Ortofoto_Mentah"
folder_keluar = HASIL + "/NDVI_latar"
os.makedirs(folder_keluar, exist_ok=True)


def hitung_ndvi(task, folder_in, folder_out):
    """Berjalan di LATAR BELAKANG. Jangan menyentuh proyek atau antarmuka di sini."""
    daftar = sorted(glob.glob(folder_in + "/*.tif"))
    keluar = []
    for i, jalur in enumerate(daftar):
        if task.isCanceled():                       # pengguna menekan Cancel
            return None
        tujuan = folder_out + "/NDVI_" + os.path.basename(jalur)
        ubin = QgsRasterLayer(jalur, "ubin", "gdal")
        processing.run("native:rastercalc", {
            "EXPRESSION": '("ubin@1" - "ubin@2") / ("ubin@1" + "ubin@2")',
            "LAYERS": [ubin], "OUTPUT": tujuan})
        keluar.append(tujuan)
        task.setProgress(100 * (i + 1) / len(daftar))    # bilah kemajuan di pojok kanan bawah QGIS
    return keluar


def selesai(exception, hasil=None):
    """Berjalan di PROGRAM UTAMA setelah tugas berakhir. Aman untuk menyentuh proyek."""
    if exception is not None:
        QgsMessageLog.logMessage("Tugas gagal: %s" % exception, "Batch NDVI", Qgis.Critical)
    elif hasil is None:
        QgsMessageLog.logMessage("Tugas dibatalkan", "Batch NDVI", Qgis.Warning)
    else:
        for jalur in hasil:
            nama = os.path.basename(jalur)[:-4]
            QgsProject.instance().addMapLayer(QgsRasterLayer(jalur, nama, "gdal"))
        QgsMessageLog.logMessage("Selesai: %d berkas" % len(hasil), "Batch NDVI", Qgis.Success)
        print("Tugas selesai:", len(hasil), "berkas NDVI ditambahkan ke proyek")


tugas = QgsTask.fromFunction("Hitung NDVI semua ubin", hitung_ndvi, on_finished=selesai,
                             folder_in=folder_masuk, folder_out=folder_keluar)
QgsApplication.taskManager().addTask(tugas)
print("Tugas dikirim ke latar belakang. QGIS tetap bisa dipakai.")

Penjelasan baris demi baris. Skrip ini terdiri atas dua fungsi dan tiga baris pengiriman. Rumus NDVI di baris 25 sama dengan Skrip 21.4.

Blok A: fungsi tugas, yang bekerja di latar belakang (baris 15-29).

BarisTulisanArtinya
15def hitung_ndvi(task, folder_in, folder_out):Fungsi tugas. Parameter pertama task diisi otomatis oleh QGIS. Dua lainnya diisi dari baris 47.
16"""Berjalan di LATAR BELAKANG..."""Catatan di awal fungsi (disebut docstring, Bab 7). Isinya peringatan: jangan menyentuh proyek atau antarmuka di sini.
17-18daftar = ... dan keluar = []Kumpulkan berkas, siapkan daftar hasil.
19for i, jalur in enumerate(daftar):Ulangi tiap ubin. enumerate menambahkan nomor urut i mulai dari 0 (Bab 6).
20-21if task.isCanceled(): return NoneBila pengguna menekan Cancel, berhenti dan kembalikan None.
22-27tujuan = ... sampai keluar.append(tujuan)Hitung NDVI satu ubin ke berkas, lalu catat jalurnya.
28task.setProgress(100 * (i + 1) / len(daftar))Isi bilah kemajuan, 0 sampai 100 persen.
29return keluarKembalikan daftar berkas hasil. Nilai ini akan diterima fungsi selesai.

Blok B: fungsi selesai, yang bekerja di depan (baris 32-43).

BarisTulisanArtinya
32def selesai(exception, hasil=None):Fungsi selesai. exception berisi galat bila tugas gagal. hasil berisi daftar dari return keluar. =None adalah nilai bawaan (Bab 7).
34-35if exception is not None:Bila ada galat, catat ke log sebagai Qgis.Critical.
36-37elif hasil is None:Bila tidak ada hasil, tugas dianggap dibatalkan. Catat Qgis.Warning.
38-43else:Bila sukses: untuk tiap jalur, buat layer dan tambahkan ke proyek (baris 39-41), catat Qgis.Success (baris 42), lalu cetak pesan (baris 43). [:-4] membuang empat huruf terakhir (.tif) dari nama berkas.

Blok C: membuat dan mengirim tugas (baris 46-49).

BarisTulisanArtinya
46-47tugas = QgsTask.fromFunction("Hitung NDVI semua ubin", hitung_ndvi, on_finished=selesai, folder_in=..., folder_out=...)Bungkus fungsi tugas menjadi tugas. Isian: judul, fungsi tugas, fungsi selesai (on_finished), lalu argumen bernama yang diteruskan ke fungsi tugas. Fungsi dipakai sebagai nilai tanpa kurung.
48QgsApplication.taskManager().addTask(tugas)Serahkan tugas ke manajer tugas QGIS. Tugas mulai berjalan.
49print("Tugas dikirim ...")Cetak pesan. Pesan ini muncul sebelum tugas selesai, karena skrip tidak menunggu.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
Tugas dikirim ke latar belakang. QGIS tetap bisa dipakai.
Tugas selesai: 3 berkas NDVI ditambahkan ke proyek

Pesan pertama muncul langsung. Pesan kedua muncul beberapa saat kemudian, setelah tugas selesai. Di dalam QGIS, kemajuan tugas tampil di bilah status dan pesan lognya di panel Log Messages dengan tag Batch NDVI [CEK: tampilan visualnya, tidak dapat diuji tanpa antarmuka QGIS]. Tiga layer baru NDVI_ubin_01 sampai NDVI_ubin_03 masuk ke proyek.

Bagian yang boleh diubah.

  • Baris 8-9, jalur. Sesuaikan dengan komputer Anda.
  • Baris 17, daftar berkas. Ubah pola berkas atau batasi jumlahnya.
  • Baris 25, rumus. Sesuaikan urutan band.
  • Baris 35, 37, dan 42, tag log "Batch NDVI". Ganti dengan nama yang Anda suka.
  • Baris 46, judul tugas. Judul ini tampil di daftar tugas QGIS.

Jebakan.

  • Skrip tidak menunggu tugas. Pesan "Tugas dikirim" tampil sebelum NDVI selesai. Itu normal di dalam QGIS.
  • Skrip mandiri harus menunggu sendiri. Pada uji, skrip ini dijalankan dengan kerangka mandiri, lalu dibiarkan habis begitu saja. Layar hanya menampilkan Tugas dikirim ..., kemudian proses berhenti mendadak (kode keluar 139, tanda proses mogok). Pesan "Tugas selesai" tidak pernah muncul. Perbaikannya: sebelum app.exitQgis(), tunggu tugas dengan perulangan berikut. Dengan perulangan ini, uji berjalan normal dan menampilkan kedua pesan.
PYTHONUnduh berkas
import time

while QgsApplication.taskManager().countActiveTasks() > 0:
    app.processEvents()
    time.sleep(0.05)
for _ in range(20):
    app.processEvents()
    time.sleep(0.02)
  • Fungsi tugas tidak boleh menyentuh proyek atau antarmuka. Komentar di baris 16 dan 33 dan Buku I4 mengingatkan bahwa menambah layer dari fungsi tugas dapat membuat QGIS macet atau tertutup sendiri. Itu sebabnya addMapLayer ada di fungsi selesai. Bagian ini tidak diuji [CEK].
  • Berkas pendamping. Seperti Skrip 21.4, QGIS dapat membuat berkas .aux.xml di folder NDVI_latar.

Latihan variasi. Batasi tugas hanya untuk dua ubin pertama.

Contoh jawaban (sudah diuji). Ubah baris 17 menjadi:

PYTHON
    daftar = sorted(glob.glob(folder_in + "/*.tif"))[:2]

[:2] mengambil dua isi pertama dari daftar. Layar menampilkan Tugas selesai: 2 berkas NDVI ditambahkan ke proyek, dan folder NDVI_latar berisi NDVI_ubin_01.tif dan NDVI_ubin_02.tif.

Cek paham #

  1. Apa isi hasil["OUTPUT"] bila OUTPUT berupa "TEMPORARY_OUTPUT" pada alat vektor? Bagaimana bila alatnya raster?
  2. Mengapa CHM dihitung di luar perulangan pada Skrip 21.5?
  3. Fungsi mana yang boleh menambahkan layer ke proyek pada Skrip 21.6, dan mengapa?

Jawaban.

  1. Pada alat vektor, isinya layer (QgsVectorLayer) yang siap dipakai. Pada alat raster, isinya teks jalur berkas, yang harus dibungkus dengan QgsRasterLayer.
  2. CHM seluruh area sama untuk semua KPH. Menghitungnya sekali cukup, dan menghitungnya di tiap putaran hanya membuang waktu.
  3. Fungsi selesai. Ia berjalan di program utama, sedangkan fungsi tugas berjalan di latar belakang dan tidak boleh menyentuh proyek.

Kesalahan umum #

  • Salah ketik nama isian atau kolom. Nama isian yang tak dikenal diabaikan diam-diam (Skrip 21.1), sedangkan nama kolom yang salah memicu KeyError. Perbaikan: salin nama dari parameterDefinitions() dan dari daftar kolom layer.
  • Mengira layer yang gagal dibuka akan menimbulkan galat. Alat tetap jalan dengan layer kosong dan menghasilkan angka nol. Perbaikan: periksa isValid() dan baca baris pertama hasil.
  • Urutan band terbalik. NDVI menjadi negatif tanpa pesan apa pun. Perbaikan: cek urutan band tiap dataset dan curigai tanda minus.
  • Memakai satuan jarak yang salah. DISTANCE mengikuti CRS, jadi 30 pada CRS derajat berarti 30 derajat. Perbaikan: pastikan CRS metrik.
  • Skrip mandiri yang tidak menunggu tugas latar belakang. Prosesnya berhenti mendadak sebelum tugas selesai. Perbaikan: tambahkan perulangan tunggu.

Ringkasan dan latihan #

  • processing.run("ID_ALAT", {kamus parameter}) memanggil alat Processing. Hasilnya kamus, dan jawabannya diambil dengan kunci seperti ["OUTPUT"].
  • Vektor sementara berupa layer. Raster sementara dan keluaran berupa berkas berupa teks jalur.
  • Merangkai alat berarti memakai hasil satu alat sebagai masukan alat berikutnya.
  • Batch berarti satu resep, banyak masukan: perulangan atas berkas (glob) atau atas fitur (getFeatures).
  • QgsTask menjalankan pekerjaan berat di latar belakang. Fungsi tugas menghitung, fungsi selesai menyentuh proyek.

Latihan. Gabungkan Skrip 21.2 dan 21.5. Untuk tiap KPH, buat sempadan sungai 30 meter, lalu potong CHM dengan sempadan itu, dan cetak rata-rata tinggi tajuk di sempadan. Alat apa yang Anda butuhkan di setiap langkah, dan bagian mana yang dipinjam dari tiap skrip?

Tabel perbandingan: memanggil alat dengan klik dan dengan skrip #

PekerjaanDengan klik di QGISDengan skrip
Memilih alatCari di Processing ToolboxID alat, misalnya native:buffer
Mengisi kolomIsi dialogKamus parameter
MenjalankanTombol Runprocessing.run(ID, kamus)
Mengambil hasilLayer baru di panel Layershasil["OUTPUT"], lalu addMapLayer
Banyak berkasExecute as batch processPerulangan for dengan glob
Pekerjaan beratAlat berjalan, QGIS terasa macetQgsTask di latar belakang