5.1. Objek Image¶
Algoritma pemrosesan citra berjalan melintasi sebuah citra satu piksel pada satu waktu. Di setiap posisi, ia melakukan sesuatu yang sederhana -- membaca nilai, membandingkannya dengan ambang batas, menggabungkannya dengan piksel yang sesuai dari citra kedua, menulis kembali hasilnya. Diulang di seluruh bingkai, keputusan per-piksel sederhana tersebut adalah fondasi dari deteksi tepi, pelacakan blob, dekoding kode QR, dan setiap teknik visi komputer klasik lainnya. Agar pekerjaan itu berjalan efisien, algoritma harus mengetahui di mana setiap piksel berada dalam memori, apa arti nilai setiap piksel sebenarnya, dan bagian citra mana yang harus dilihatnya. image.Image adalah objek yang mengorganisasikan informasi tersebut.
Vision Sensors berakhir pada saat csi.CSI.snapshot() mengembalikan nilai. Apapun yang dilakukan mekanisme sisi kamera untuk menghasilkan bingkai yang ditangkap sudah selesai; aplikasi memegang Image dan perlu mengetahui apa yang harus dilakukan dengannya.
5.1.1. Buffer dan propertinya¶
Di dalam Image terdapat pointer ke blok byte yang berurutan dalam RAM dan header kecil yang memuat tiga bagian metadata: lebar citra dalam piksel, tingginya dalam piksel, dan format piksel yang digunakan byte tersebut. Byte-byte tersebut adalah piksel itu sendiri, disimpan dalam urutan baris-utama -- semua piksel baris teratas terlebih dahulu, kemudian semua piksel baris kedua, dan seterusnya hingga bagian bawah. Properti-properti tersebut menggambarkan cara membacanya.
Lebar dan tinggi adalah hitungan bilangan bulat biasa. Format piksel adalah properti yang lebih menarik, karena menentukan berapa byte yang diambil setiap piksel dan apa yang dikodekan oleh byte tersebut. Citra skala abu-abu membawa satu byte per piksel yang menyimpan nilai kecerahan. Citra RGB565 membawa dua byte per piksel yang menyimpan field merah, hijau, dan biru yang dikemas dalam kata 16-bit. Citra Bayer membawa satu byte per piksel, tetapi setiap piksel disampling melalui salah satu dari tiga filter warna yang dipilih berdasarkan posisinya dalam mosaik. Vision Sensors telah mengenumerasi seluruh katalog; yang penting di sini adalah bahwa tepat satu dari format tersebut ditetapkan pada setiap Image, dan pilihan tersebut menentukan aritmetika byte-per-piksel dan makna dari setiap byte tunggal dalam buffer.
Dengan sebuah pointer ke buffer, lebar, tinggi, dan format, setiap properti lain yang mungkin diinginkan algoritma dapat diperoleh melalui perhitungan singkat. Byte yang mengawali piksel (x, y) berada pada offset (y * width + x) * bytes_per_pixel dari awal buffer. Total jumlah byte adalah width * height * bytes_per_pixel. Alamat baris berikutnya tepat width * bytes_per_pixel byte setelah awal baris saat ini. Image mengekspos tiga properti melalui pemanggilan metode biasa -- width(), height(), format() -- ditambah size turunan melalui size(). Metode-metode lain dalam modul menggunakan nilai-nilai tersebut untuk melakukan aritmetika offset sendiri; kode aplikasi jarang harus melakukannya.
Sebuah Image adalah pembungkus Python kecil yang menunjuk ke blok memori yang berurutan: header yang memuat lebar, tinggi, dan format piksel, diikuti oleh buffer piksel itu sendiri.¶
5.1.2. Darimana buffer berasal¶
Kisah default sepanjang bab ini adalah yang sudah dibahas Vision Sensors: bingkai yang ditangkap tiba dari snapshot, byte-byte tersebut berada di buffer bingkai kamera, dan Image yang dikembalikan menunjuk ke sana. Tiga cara lain untuk mendapatkannya muncul secara teratur, dan masing-masing mengimplikasikan sesuatu yang berbeda tentang di mana buffer tersebut berakhir.
Memuat dari file terlihat seperti meneruskan path ke konstruktor: image.Image("/sdcard/saved.jpg"). Modul membaca file ke buffer yang baru dialokasikan di heap Python. File BMP, PGM, dan PPM didekode saat dibaca dan Image yang dihasilkan membawa format piksel yang tidak terkompresi. File JPEG dan PNG tetap terkompresi -- Image membawa format JPEG atau PNG, dan buffer menyimpan aliran byte file pada dasarnya tanpa perubahan. Untuk melakukan pekerjaan tingkat piksel pada citra yang terkompresi, aplikasi mengonversinya terlebih dahulu melalui to_rgb565() atau to_grayscale(), dan konversi itulah tempat dekompresi -- dan pembengkakan heap yang sesuai, di mana JPEG 30 KB dapat menjadi 600 KB RGB565 -- sebenarnya terjadi. Memuat dari file paling berguna selama pengembangan, ketika sebuah algoritma perlu diuji terhadap bingkai referensi yang diketahui yang disimpan bersama skrip.
Membangun satu dari awal adalah kasus kanvas: image.Image(320, 240, image.RGB565) meminta modul untuk mengalokasikan sejumlah byte tersebut dalam format tersebut, mengosongkan konten, dan mengembalikan pembungkusnya. Piksel-piksel belum memiliki arti -- semuanya nol -- tetapi citra kosong adalah andalan untuk beberapa pola yang berulang: bingkai referensi yang digunakan untuk mengurangkan bingkai saat ini, kanvas tempat overlay grafis disusun, buffer biner yang diisi dan digunakan sebagai masker.
Konstruksi dari ndarray menjembatani ke arah lain, dari komputasi numerik apa pun kembali ke modul citra. Meneruskan ulab.numpy.ndarray float32 ke konstruktor menghasilkan Image yang dimensinya cocok dengan ndarray -- bentuk dua-sumbu (h, w) menjadi citra skala abu-abu, bentuk tiga-sumbu (h, w, 3) menjadi RGB565 -- dengan nilai float yang diskalakan dari 0.0 -- 255.0 ke rentang piksel integer. Peta panas jaringan saraf, array numerik dari jenis apa pun, apa pun yang dihasilkan oleh ml atau ulab menjadi sesuatu yang dapat digunakan oleh sisi gambar dan inspeksi modul citra.
Keempat sumber tersebut mengembalikan jenis Image yang sama. Kode yang menggunakan objek yang dikembalikan tidak perlu melacak dari mana asalnya.
5.1.3. Dua tampilan atas byte-byte¶
Sebagian besar waktu, kode aplikasi memperlakukan Image sebagai objek citra bertipe -- sesuatu dengan metode bernama. Sisi lain dari cerita ini adalah bahwa objek yang sama juga muncul, secara transparan, sebagai urutan byte datar ke API MicroPython apa pun yang menerima argumen bytes. Byte-byte tersebut bukan salinan buffer; melainkan tampilan langsung ke dalamnya.
Pengaturan tersebut adalah yang membuat pengiriman bingkai yang ditangkap dari kamera menjadi satu baris kode. Melakukan hashing, mengirimkannya melalui port serial, meneruskannya ke socket jaringan -- tidak ada satupun yang memerlukan langkah "konversi citra ke byte" yang terpisah:
import csi
import hashlib
csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QQVGA)
img = csi0.snapshot()
uart.write(img) # transmits the raw pixel bytes
hashlib.sha256(img) # hashes the same bytes
sock.send(img) # sends them over a socket
Tampilan bytes-like bersifat read-only secara default, dengan sengaja. Buffer citra berukuran besar dan terkadang dibagikan antara lapisan-lapisan tumpukan pencitraan, sehingga memberi kekuatan pada buf[0] = 0 yang ceroboh di suatu tempat jauh di dalam tumpukan panggilan untuk secara diam-diam merusak salah satunya adalah tepi yang terlalu tajam untuk dibiarkan terbuka. Ketika akses byte-level baca-tulis adalah yang sebenarnya dibutuhkan aplikasi -- menulis nilai kalibrasi ke offset yang diketahui, misalnya -- bytearray() mengembalikan tampilan terpisah yang secara eksplisit baca-tulis atas memori yang sama, menandai niat tersebut di situs pemanggilan.
5.1.4. Di mana buffer berada¶
Buffer piksel cukup besar sehingga tempat mereka berada di RAM menjadi penting. Sebuah bingkai RGB565 QQVGA adalah 160 × 120 × 2 = 38.400 byte; bingkai RGB565 VGA adalah 614.400 byte; input RGB565 224 × 224 yang mungkin dikonsumsi oleh classifier jaringan saraf sekitar 100 KB. Heap Python pada kamera terkecil hanya bisa beberapa puluh kilobyte setelah runtime melakukan boot. Menyimpan lebih dari satu atau dua bingkai data citra di heap akan menghalangi segalanya.
Jalan keluarnya adalah bahwa buffer citra sebagian besar tidak berada di heap Python. Mereka berada di wilayah RAM khusus yang diperkenalkan Vision Sensors sebagai frame buffer -- memori yang sama tempat DMA kamera menulis bingkai yang ditangkap dan pratinjau IDE membaca bingkai yang selesai. Sebagian besar operasi pada Image memodifikasi sumbernya di tempat: algoritma membaca piksel, memutuskan, menulis nilai baru kembali, dan tidak ada citra hasil terpisah yang dialokasikan. Operasi yang memang menghasilkan hasil terpisah -- konversi format dan beberapa lainnya -- dapat diminta untuk menempatkan hasil tersebut di buffer bingkai melalui argumen kata kunci copy_to_fb. copy_to_fb=True melakukan dua hal sekaligus: menempatkan citra hasil ke buffer bingkai daripada di heap (menghindari tekanan heap) dan menjadikan hasilnya bingkai berikutnya yang akan ditampilkan oleh pratinjau IDE. Menambahkan copy_to_fb=True pada langkah akhir pipeline, melihat hasilnya muncul di layar, dan melakukan iterasi dari sana adalah salah satu idiom debugging paling berguna dalam pemrosesan citra.
Dengan pembungkus yang menyimpan buffer berlabel, empat cara untuk membuatnya ada, dua tampilan atas byte-nya, dan sebuah sakelar yang menentukan di mana yang baru akan mendarat, Image bukan lagi misteri. Pertanyaan-pertanyaan fundamental yang tersisa -- bagaimana posisi piksel dinamai, apa yang sebenarnya disimpan setiap piksel, bagaimana membatasi operasi pada sebagian dari satu -- dibangun di atasnya.