Na dzisiejszych zajęciach zajmiemy się projektowaniem prostego interfejsu graficznego wykorzystującego Tkinter oraz OpenCV. Zacznijmy od zadeklarowania potrzebnych nam bibliotek:
from tkinter import *
from tkinter import ttk, filedialog
import numpy as np
import io
import cv2
import matplotlib
from PIL import Image
from PIL import ImageTk
"TkAgg")
matplotlib.use(from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
W ramach przypomnienia podstaw Pythona - Struktura kodu powinna najlepiej wyglądać w ten sposób, aby zachować czytelność kodu:
- importy
- deklaracje klas i funkcji
- ewentualny pozostały kod
Dlatego jeżeli plik zawiera poza funkcjami wykonywalny fragment np. if __name__ == "__main__":
lub normalny skrypt to powinien on znajdować się na końcu pliku, za wszystkimi zadeklarowanymi funkcjami.
UWAGA POCZĄTKOWA 1 Wszystkie rozmiary jak również lokalizacje w strukturze siatki (grid), są przykładowe jeżeli nie mieszczą się wam one na monitorze lub nie pasuje wam sam układ można śmiało to modyfikować. Większość kodu stara się być niezależna od ich samego położenia.
UWAGA POCZĄTKOWA 2 We wstawianych fragmentach kodu pojawiają się sekcje:
def nazwa():
############################
Oznaczają one, że rozwijamy funkcję nazwa
i wprowadzamy w niej modyfikacje. Zwykle polega to na dopisywaniu podanych linii na końcu funkcji, ale nie zawsze tak jest. Czasami modyfikowany będzie fragment w jej centrum. Domyślnie jeżeli nic nie jest napisane to w dalszym ciągu edytujemy poprzednią funkcję.
UWAGA POCZĄTKOWA 3 Wszystkie funkcje poza ostatnią main
są częścią tworzonej klasy.
Początkowa struktura
Pracę na nad naszym interfejsem zacznijmy od zadeklarowania klasy będącej kontenerem naszego interfejsu. Znajdziemy w niej dwie zmienne do przechowywania obrazów IMG
oraz TempIMG
oraz funkcję tworzenia interfejsu (aktualnie zawiera tylko wywołanie funkcji print
umożliwiające uruchomienie jej). Na końcu znajduje funkcja wywołująca nasz interfejs. Po wywołaniu poniższego kodu powinno wyświetlić się niewielkie okienko niezawierające niczego.
class Widget(Frame):
= None
IMG = None
TempIMG def __init__(self,parent=None):
__init__(self,parent)
Frame.self.parent = parent
self.pack()
self.make_widgets()
def make_widgets(self):
print('TO DO')
if __name__ == "__main__":
= Tk()
root = Widget(root)
something root.mainloop()
Dodajmy trochę treści do naszego interfejsu. Zmieńmy tytuł naszego okna i ustalmy jakiś wstępny rozmiar:
def make_widgets(self):
self.winfo_toplevel().title("GUI")
self.winfo_toplevel().geometry("1200x600")
Następnie utwórzmy obszar w którym będziemy wyświetlać wczytane przez nas zdjęcie oraz kontener Canvas
który wykorzystamy do wyświetlania oraz zakotwiczmy w nim miejsce na zdjęcie. Funkcja .grid
pozwala nam osadzić obiekt w domyślnej siatce (można modyfikować jej gęstość), jak również ustalić jak dużą jej część ma zajmować.
def make_widgets(self):
############################
self.LeftFrame=LabelFrame(self,text="Oryginal image",height=500,width=300)
self.LeftFrame.grid(row=0,column=0,columnspan=5,rowspan=5)
self.ImagePanel=Canvas(self.LeftFrame,height=500,width=300)
self.ImagePanel.pack(expand=YES, fill=BOTH)
self.ImageOnPanel=self.ImagePanel.create_image(0,0 ,anchor=NW)
Teraz musimy utworzyć zestaw dwóch funkcji dla naszej klasy. Pierwsza z funkcji ma za zadanie wywołać okno dialogowe służące do wczytania pliku graficznego:
def select_image(self):
= filedialog.askopenfilename(initialdir = "/",title = "Select file",filetypes = (
filename "jpeg files","*.jpg"),
("png files","*.png"),
("all files","*.*")
(
))if len(filename) > 0:
print(filename)
= cv2.imread(filename)
tmp self.IMG = IMG = cv2.cvtColor(tmp, cv2.COLOR_BGR2RGB)
self.show_pic()
Druga natomiast ma za zadanie wyświetlić nasze zdjęcie wewnątrz interfejsu, jeżeli już takie wczytamy. Wykorzystamy do tego dwie funkcje przekształcające obraz najpierw do formatu Image
z biblioteki PIL
a następnie ten obraz do formatu ImageTk
, który możemy już wyświetlić. Zapisujemy jego kopię wewnątrz struktury, aby uniknąć problemów z automatycznym czyszczeniem zmiennych (garbage collector).
def show_pic(self):
if self.IMG.size >0:
= Image.fromarray(self.IMG)
img self.ImagePanel.ImgCatch = ImageTk.PhotoImage(img)
self.ImagePanel.itemconfigure(self.ImageOnPanel, image=self.ImagePanel.ImgCatch)
Teraz dodajmy przycisk, za pomocą którego dodamy obraz do programu.
def make_widgets(self):
############################
self.Load=Button(self, text="Select an image", command=self.select_image)
self.Load.grid(row=6,column=0)
Na tym etapie powinniśmy mieć prosty interfejs, który otwiera i wyświetla plik graficzny po naciśnięciu przycisku.
Skalowanie obrazów przed wyświetleniem
Gdy wczytany przez nas obraz będzie za duży w naszym oknie zostanie wyświetlona tylko jego część. Można temu zapobiec przeskalowując go przed wyświetleniem. Można to zrobić wprost zmieniając obraz tak aby wypełnił cały obszar wyświetlania lub tak my planujemy to zrobić przeskalować zachowując oryginalny stosunek wzajemny długości obu boków obrazu (aspect ratio). Zacznijmy rozwijanie naszej funkcji wyświetlania od pobrania informacji na temat dostępnego nam obszaru wyświetlania:
def show_pic(self):
if self.IMG.size >0:
= Image.fromarray(self.IMG)
img =self.ImagePanel.winfo_height()
h=self.ImagePanel.winfo_width() w
W zmiennych w
oraz h
będziemy przechowywać rozmiar naszego pola wyświetlania. Teraz musimy sprawdzić jaka część naszego obrazu będzie się wewnątrz naszego obszaru wyświetlania. Informacje na temat rozmiaru naszego obrazu będziemy brać bezpośrednio ze zmiennej self.IMG
:
= h/self.IMG.shape[0]
h_ratio= w/self.IMG.shape[1] w_ratio
Jeżeli obraz mieści nam się w całości w jakimś z tych dwóch wymiarów wartość ratio
dla niego będzie >1
. Natomiast jeżeli zmieści się tylko jego fragment będzie zawierał on wartość <1
. Jeżeli jeden lub więcej współczynników jest mniejszy niż 1
, to musimy podjąć decyzję, który z wymiarów należy bardziej zmniejszyć. Będzie miał on mniejszy współczynnik. Drugi wymiar otrzyma maksymalny możliwy dostępny rozmiar, czyli wielkość naszego obszaru rysowania. Rozmiar mniejszego możemy wyliczyć przy użyciu proporcji. Do samego przeskalowania będziemy wykorzystywać funkcję .resize
będącą częścią pakietu PIL
. Gotowy fragment odpowiedzialny za skalowanie obrazu wyświetlania powinien wyglądać tak:
if (h_ratio<1.0) | (w_ratio<1.0):
if h_ratio<w_ratio:
=h_ratio*w/w_ratio
ratio= img.resize((round(ratio),round(h)), Image.ANTIALIAS)
img else:
=w_ratio*h/h_ratio
ratio= img.resize((round(w),round(ratio)), Image.ANTIALIAS) img
Filtracja obrazu
Dodajmy kolejne elementy do naszego interfejsu tym razem będzie to funkcja, która w kolejnym panelu wyświetli nam obraz poddany działaniu filtru krawędziowego. Zacznijmy od stworzenia dodatkowego obszaru wyświetlania w tej samej formie jak w przypadku oryginalnego obrazu.
def make_widgets(self):
############################
self.RightFrame=LabelFrame(self,text="Modified image",height=500,width=300)
self.RightFrame.grid(row=0,column=5,columnspan=5,rowspan=5)
self.ImagePanel2= Canvas(self.RightFrame,height=500,width=300)
self.ImagePanel2.pack(expand=YES, fill=BOTH)
self.ImageOnPanel2=self.ImagePanel2.create_image(0,0 ,anchor=NW)
W przygotowaniu do późniejszych zadań dodajmy również listę rozwijaną pozwalającą na wybór przekształcenia:
def make_widgets(self):
############################
self.Choose = ttk.Combobox(self,values=["Edges"])
self.Choose.current(0)
self.Choose.grid(row=6,column=1,rowspan=3)
Teraz napiszmy, krótką funkcję realizującą nasz filtr krawędziowy, wykorzystując OpenCV
, a na końcu wywoła nasze wyświetlanie.
def confirm(self):
if(self.Choose.current() == 0):
self.TempIMG = cv2.Canny(self.IMG, 50, 100)
self.show_pic()
I na koniec dołączmy do naszej przycisk, który wywoła naszą funkcję.
def make_widgets(self):
############################
self.Confirm = Button(self, text="Confirm", command=self.confirm)
self.Confirm.grid(row=6,column=4)
Ostatnią rzeczą jaką musimy dopisać jest dodanie fragmentu do funkcji wyświetlania. Funkcję należy jednak uzupełnić o fragment odpowiadający za skalowanie obrazu.
def show_pic(self):
############################
if self.TempIMG in not None and self.TempIMG.size >0:
= Image.fromarray(self.TempIMG)
img ##### <- tu dopisać skalowanie
self.ImagePanel2.ImgCatch = ImageTk.PhotoImage(img)
self.ImagePanel2.itemconfigure(self.ImageOnPanel2, image=self.ImagePanel2.ImgCatch)
Wyświetlanie grafów i wykresów
Rozwińmy nasz interfejs o fragment pozwalający wyświetlać nam wykresy. W tym przypadku wyświetlający histogram dla poszczególnych składowych kolorów. Zacznijmy od dodania kolejnego obszaru, w który będzie nam służył do wyświetlania. Dodajemy w nim strukturę Figure
i montujemy w niej jeden subplot
, w której będziemy rysować.
def make_widgets(self):
############################
self.PlotFrame=LabelFrame(self,text="Plot",height=500,width=300)
self.PlotFrame.grid(row=0,column=10,columnspan=5,rowspan=5)
self.Fig = Figure()
self.Plot = self.Fig.add_subplot(1,1,1)
self.canvas = FigureCanvasTkAgg(self.Fig, self.PlotFrame)
self.canvas.draw()
self.canvas.get_tk_widget().pack()
Następnym krokiem jest stworzenie funkcji, która wyliczy nam i wyrysuje nam histogram. Również do tego wykorzystamy funkcję z pakietu OpenCV
. Musimy pamiętać również żeby na początku wyczyścić wykres (funkcja .cla()
). Histogram liczymy i rysujemy histogram dla każdego koloru osobno. Na końcu dodajemy wywołanie funkcji rysującej.
def calc_hist(self):
if self.IMG.size >0:
self.Plot.cla()
= ('r','g','b')
color for i,col in enumerate(color):
= cv2.calcHist([self.IMG],[i],None,[256],[0,256])
histr self.Plot.plot(histr,color = col)
self.show_pic()
Jak mamy już gotowe wywołanie możemy podpiąć je do nowo utworzonego przycisku.
def make_widgets(self):
############################
self.Hist = Button(self, text="Calculate Histogram", command=self.calc_hist)
self.Hist.grid(row=6,column=11)
Ostatnim krokiem jest dodanie ponownego rysowania wykresu do odświeżającej.
def show_pic(self):
############################
self.canvas.draw()
Binaryzacja z parametrem
Ostatnim krokiem ćwiczenia jest dodanie funkcji wykorzystującej do działania parametry. Wykorzystamy w tym celu funkcję realizującą binaryzację z progiem. Dzięki dobremu przemyśleniu konstrukcji naszego interfejsu dodanie nowej funkcjonalności wymaga wykonania niewielkiej ilości modyfikacji. Pierwszym krokiem będzie dodanie poziomego suwaka, który będzie dostarczał nam wartość parametru binaryzacji i ustawmy jednego domyślną wartość na 128
.
def make_widgets(self):
############################
self.Slider = Scale(self, from_=0, to=255, orient=HORIZONTAL)
self.Slider.set(128)
self.Slider.grid(row=6,column=5)
Następnie dodajmy możliwość wyboru binaryzacji jako opcji w naszym menu poprzez modyfikację jego definicji.
def make_widgets(self):
############# NIE DOKLEJAĆ MODYFIKOWAĆ
self.Choose = ttk.Combobox(self,values=["Edges","Binary Threshold"])
Ostatnim krokiem jest dodanie obsłużenie nowo dodanej funkcjonalności. Zrobimy to poprzez modyfikację confirm
poprzez dodanie wywołania odpowiedniej OpenCV
. Funkcja cv2.threshold
przyjmuje jako parametr obraz w skali odcieni szarości (obraz kolorowy również można wykorzystać, ale efekt będzie nakładany za każdą warstwę osobo) oraz próg. Do szybkiej konwersji naszego obrazu oryginalnego do skali odcieni szarości również wykorzystamy funkcję z OpenCV
, a parametr progu pobierzemy bezpośrednio z suwaka. Gotowa modyfikacja powinna wyglądać tak:
def confirm(self):
############################
if(self.Choose.current() == 1):
=cv2.threshold(
res,retself.IMG, cv2.COLOR_RGB2GRAY),
cv2.cvtColor(self.Slider.get(),255,cv2.THRESH_BINARY)
self.TempIMG=ret
Zadania
Oprócz już pokazanych już w instrukcji funkcjonalności:
- filtracja krawędziowa
- binaryzacja z progiem
- histogram
Dodać kolejne funkcjonalności z OpenCV przykładowo (o wywołaniach przeczytać w dokumentacji do OpenCV):
- inne rodzaje binaryzacji
- wyświetlanie obrazu w skali odcieni szarości
- binaryzacja z adaptacyjnym progiem
- różne rozmycia (blur)
Wszystkie rodzaje funkcji jeżeli wymagają parametrów powinny czerpać je z GUI (np. z suwaka).