Most of the time my recent job is about analysing images made by drones or satellites. Based on my Python experience that I have already acquired earlier I can look inside also the fascinating field of precision agriculture. I am beginner in this field but I have already solved some basic problem for example measuring coloured pixels and further statistical computings. You can find some Python codes here. First of all an example for Euclidean distance measuring. After that I am going to present my k-means clustering example applied on satellite images.
Info! The explanations you can read below are written in Hungarian language, but I pushed the codes to GitHub as well and attached textfiles with brief documentations in English. Here is the link: github.com/pixel_analysis
A következő Python példa egy olyan bővíthető képelemző program, amely adott kép pixeleiről gyűjt adatokat és információkat, azzal a szándékkal, hogy támogassa a képeket felhasználó szakemberek munkáját. A fejezetben szereplő kód egy képelemző program bővíthető alapját, vázát kívánja bemutatni. A kódot igyekeztem az objektum orientált programozás paradigmájának megfelelően megírni. Az elsőként látható kódrészletben az Analysis osztály inicializálása látható. Ennek megvalósítása a konstruktor meghívásával [ __init__() ] illetve egy saját függvény létrehozásával történt [ _init_image() ].
import os
import math
from PIL import Image
class Analysis:
# For all instances
# Adding headers to the lists of results
distSum, distPerc = ["px_found"], ["(%)"]
fname = ["Image"]
" Initialize the programme "
def __init__(self, path,plant,sample,limit):
self.path = path
self.img_path = f"{self.path}images\\"
self.plant = plant
self.sample = sample
self.limit = limit
# Walking through the filenames
self.img_files = next(os.walk(self.img_path))[2]
def __init_image(self,fname,mode=None):
""" Opening new image file & getting attributes.
Warning: After calling this, you have to close the file! """
self.image = Image.open(fname).convert(mode)
self.pix = self.image.load()
self.width, self.height = self.image.size
self.imsize = self.width*self.height
Az __init__ – ben elsősorban azok az objektummal, ez esetben képfájllal kapcsolatos információk vannak, amelyekkel az osztály függvényei, metódusai az elemzés során dolgoznak. A saját _init_image függvény tartalmazza a program által felhasználandó kép megnyitási parancsát, pixeleinek betöltését, szélességét, magasságát és a kép mérétet. Az _init_image létrehozása elsősorban abból a célból történt, hogy a különböző metódusok számára a képek saját példányban, saját felhasználásra is elérhetők legyenek. A következő kódrészlet az osztály metódusaira hoz példákat.
" METHOD type:1 "
" Make directory if not exists "
def mkdirIfNotExists(self, folderPath):
if os.path.exists(folderPath) == False:
os.mkdir(folderPath)
else:
pass
" Get percent "
def percent(self, elem):
return format(elem/self.imsize*100,'.2f')
# You can add more method here ...
" METHOD type:2 "
" 1. Method: Euclidean distance "
def __euclidean(self, px):
res, channel = 0, 3 # R-G-B
for i in range(channel):
res = res + (self.sample[i]-px[i])**2
return math.sqrt(res)
def distance(self):
db = 0
for i in range(self.width):
for j in range(self.height):
if self.__euclidean(self.pix[i,j])<=self.limit:
db += 1
return db
# You can add more method here ...
Alkalmazási céljukat tekintve az itt bemutatandó osztály kétfajta metódust tartalmaz. Az egyik fajta metódus (“METHOD type:1”) technikailag és formailag támogatja a programot, míg a másik fajta metódus (“METHOD type:2”) a program lényegi részét támogatja, vagyis képelemző algoritmusokat tartalmaz. Itt metódusfajtánként 2-2 egyszerű példát mutatok be a szemléltetés kedvéért: A “METHOD type:1” -ben egy mappalétrehozó metódus – a program a képek mentéséhez mappákat hoz létre – illetve egy számítást végző metódus szerepel. Az elemző metódusoknál a “METHOD type:2”-ben viszont egy euklidészi távolságmérő módszert láthatunk (euclidean), ami egy képet bejáró metódusba épül be (distance) és az adott távolságba eső pixelek darabszámával tér vissza. A pixelhalmazt egy választott RGB érték (pl. jellemző szín, keresett szín) és az ehhez képest való távolságmérések határozzák meg (lásd: __main__: sample és limit változók). A távolságpixelek százalékos előfordulási gyakorisága ezen darabszám illetve a kép mérétének arányának felel meg. Az alábbi kódrészlet a program végrehajtó komponensét mutatja be. Itt történik a metódusok meghívása, majd a keletkezett adatok/eredmények listákba olvasása, végül fájlba írása táblázat formájában.
" RUN ANALYSIS "
def run_analysis(self):
for f in self.img_files:
" New image "
self.__init_image(self.img_path+f)
" Execute methods "
self.distSum.append(self.distance()) # amount of relevant pixels
self.distPerc.append(self.percent(self.distance())) # %
# You can add more method to run here ...
" Handling image file "
self.fname.append(f) # filename
self.image.close()
" lists of datas -> as columns "
self.res_data = (
self.fname,
self.distSum,
self.distPerc
# Add more results/list here ...
)
return self.result_table()
" OUTPUT -> TEXTFILE "
# Table of results
def result_table(self):
resdir = f"{self.path}\\results\\"
self.mkdirIfNotExists(resdir)
tabla = f"{resdir}results.txt"
res = open(tabla,"a+")
res.write(f"Plant type: {self.plant}\n")
for i in range(len(self.img_files) + 1): # + 1: for header
for m in self.res_data:
res.write(f"{str(m[i])}\t")
res.write("\n")
res.close
# You can add here more output formats, charts etc...
A run_analysis metódusban a képek egy megadott mappából iteratív módon kerülnek feldolgozásra. Minden iterációhoz tartozik egy-egy elemzés az euklideszi távolságra vonatkozóan (“Execute methods”) illetve a keletkezett eredmények “listázásának” lépése (“lists of datas – > as columns”), melyből az adatok táblázatos alakja is következik. Ahány elemző metódust (lásd. Method 2 az előző kódrészletben) veszünk fel a programunkba, annyi oszlopa lesz végül az eredménytáblázatunknak. A táblázat fájlbaírását a result_table függvény végzi. A program futtatása az inputok megadásával és az objektum létrehozásával pedig az alább látható programrészben történik.
if __name__ == "__main__":
" Input "
path = "c:\\Python\\Python38-32\\myprojects\\analysis_class\\"
plant = "Colza"
sample = (93,118,106)
limit = 25
" Process "
a = Analysis(path,plant,sample,limit)
a.run_analysis()
A __main__ alatti rész akkor nem kerül végrehajtásra, ha az Analysis osztályt egy másik program importálja. Továbbá megjegyzendő, hogy az inputokat a felhasználó igényeire való tekintettel legalább fájlokba mentve, de még jobb ha űrlapon keresztül adjuk meg a programnak (ez utóbbira példa a Python GUI – Tkinter fejezet).
Az alábbi képen a program által feldolgozott képek és az eredmények láthatók. A pixelelemzés tehát egy kép minden pixelének egy adott mintapixeltől [( sample = (93,118,106), ami a pixel RGB értéke], adott távolságra lévő [limit = 25] pixeleinek relatív gyakoriságát vizsgálja. Ez a gyakorlatban azt jelenti, hogy a program képes egy adott szín (RGB tartomány) előfordulásáról információt adni. A képen, a szöveges fájl nézetén a kép neve, a releváns pixelek száma és százalékos aránya látható tabulátorral elválasztva. A képek közül tehát az A01 reprezentálja leginkább a választott színt, 32,07%-ban. ( A fenti példa program a repcevirágzás mértékét vizsgálja az egyes táblákra vonatkozóan. A program úgy került kialakításra, hogy a későbbiekben további módszerekkel bővíthető legyen! Az eredmények: results.txt, és a vizsgált képek: images )
Elméleti előzmények: https://graczolbenedek.com/wp/python/pykiserlet/
RGB értékek K-közép klaszterezése
A következő program pixelek RGB értékei alapján hoz létre különböző klasztereket, annak érdekében hogy a képen látható domináns színek megbecsülhetők legyenek. A két tesztkép valójában műholdas felvételek alapján készült foltképek. Az első, saját módszer alapján készült (grayscaled), a másik pedig egy infrakép. Az output kördiagramok a klaszterezés eredményét szemléltetik, a színklaszterek %-os előfordulásának megfelelően.
Teszt1 - input

Teszt1 - output

Teszt2 - input

Teszt2 - output

Az alábbiakban egy k-közép klaszterezést implementáló kód olvasható, amely magából a módszerből, illetve annak segédfüggvényeiből áll. A klaszterezés célja esetünkben, hogy a műholdas képeken látható foltok, homogén zónák méretét megbecsülje, legalábbis ehhez segítséget nyújtson. A teszt képek fehér maszkolással készültek. A teszt1 egy szürkeárnyalatos, saját feldolgozó módszerrel készült kép, a teszt2 pedig egy infravörös vizsgálati kép. A program a __center_find illetve a __get_cldatas függvényekkel a maszkolt hátteret illetve a távolságméréssel (__distance) az elmosódásból adódó pontatlanságot kezeli. (itt: az 5-nél nem nagyobb távolságra levő 254 rgb-re értendő!). Lássuk a kódrészleteket!
import os
import cv2
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
class Clustering():
# Output data -> results
percent = []
centers = []
" Initial instance variables & methods "
def __init__(self, path,clusters):
self.path = path
self.clusters = self.__cluster_input_correction(clusters) + 1 # clusters: relevant clusters, + 1: background
self.back = (254,254,254)
self.image = cv2.imread(self.path)
self.image = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB)
self.image = self.image.reshape((self.image.shape[0] * self.image.shape[1], 3))
# Calling Clustering method
self.clustering()
" Correction & Validation methods here ... "
def __cluster_input_correction(self,clusters):
" Handle negative & zero inputs: correction to 1 "
if clusters < 1:
return 1
return clusters
Az első kódrészletben a konstruktor illetve egy kezdetleges input validálást végző metódus látható. A konstruktor paraméterként fogadja a kép útvonalát illetve a klaszterek számát, valamint megnyitja és előkészíti az elemzésre szánt képet. Az input validálás itt valójában csak annyit jelent, ha a felhasználó tévedésből 1-nél kisebb számot ad meg, akkor 1-re állítja (minimális értékként) a klaszterek számát.
" K-MEANS CLUSTERING "
" 1. Preparation "
def __distance(self, pix,color):
" Euclidean distance - detect background pixels - not only the exact color "
lim, res, channel = 5, 0, 3
for i in range(channel):
res = res + (color[i]-pix[i])**2
if res**(1/2) <= lim:
return True
return False
def __center_find(self, ndarr,bcolor):
" Find the center in the 'bcolor' domain "
ndarr = np.rint(ndarr)
for i in range(len(ndarr)):
if self.__distance(ndarr[i],bcolor):
return i
return None
def __get_cldatas(self, color,centers,clt_labels):
" Delete background color from the centers and the labels belong to "
if color is not None:
centers = np.delete(centers,color,0)
labels = np.delete(clt_labels,np.where(clt_labels == color))
else:
labels = clt_labels
return (centers, list(labels))
A fenti kódrészlet tulajdonképpen már a klaszterezési folyamat előkészítő részét ábrázolja. Mivel az input képek fehér háttérű, maszkolt képek és a klaszterezés nem terjedhet ki a háttérre, ezért szükséges azt a klaszter középpontok, illetve a középpontokhoz tartozó címkék közül eltávolítani. A __center_find a középpont keresését végzi, egy bizonyos megengedett távolsággal lásd: __distance. A megengedett távolság (lim=5) aszerint módosítható, hogy milyen mértékű elmosódás figyelhető meg a képen, különös tekintettel a releváns terület határvonalaira (.png esetében nincs!). Végül a __get_cldatas metódus törli az előzetesen kiszűrt háttér klaszter középpontját és a hozzá tartozó címkéket, annak érdekében hogy az eredményül kapott gyakoriság értékek ne torzuljanak.
" 2. Clustering "
def clustering(self):
" Info: cluster centers == array indexes: e.g. cluster_centers_[0] -> 0 "
clt = KMeans(n_clusters = self.clusters)
clt.fit(self.image)
centers = np.rint(clt.cluster_centers_)
labels = clt.labels_
# Filtering background
background = self.__center_find(centers,self.back)
# Update centers, labels with filtered background color
self.centers, labels = self.__get_cldatas(background,centers,clt.labels_)
for i in range(len(self.centers) + 1): # + 1: replace deleted background
if i != background:
j=labels.count(i)
j=j/len(labels)
self.percent.append(format(j*100,'.2f'))
plt.pie(self.percent,colors=np.array(self.centers/255),labels=self.percent)
plt.savefig(f"c:\\Python\\Python38-32\\myprojects\\pixel_analysis\\result\\cls_result")
# Results <- round down!
return self.centers.astype(int)
A klaszterezés metódus képezi a program érdemi részét, ami a fenti kódrészletben a folyamat előkészítését végző metódusok meghívásával és az adat vizualizációért felelős torta diagram elkészítésével és mentésével együtt látható. Végül az objektum létrehozásával, paraméterezésével (kép helye, klaszterek száma) a konzolra is kiiratjuk az egyes klaszter középpontokat (RGB) és az azokhoz tartozó címkék relatív gyakoriságát (%).
if __name__ == "__main__":
# Path of the image, set your own here!
img = "c:\\Python\\Python38-32\\myprojects\\pixel_analysis\\infra.png"
# Set the numbers of clusters - minimum cluster: 1, which means n<1 inputs replace with 1!
clusters = 4
cl = Clustering(img,clusters)
# Print centers to console (RGB)
print(f"\nCluster centers (RGB):\n{cl.centers}")
# Print results to console (%)
print(f"\nRelative appearances (%):\n{cl.percent}")