diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..9b6b176 Binary files /dev/null and b/.pylintrc differ diff --git a/requirements.txt b/requirements.txt index 8601087..f92b3d8 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/src/config/config.template.json b/src/config/config.template.json index a17404f..21299f1 100644 --- a/src/config/config.template.json +++ b/src/config/config.template.json @@ -1,5 +1,8 @@ { "path": "", - "out": "", - "size": 750 + "out": { + "img": "", + "log": "" + }, + "size": 100 } \ No newline at end of file diff --git a/src/helpers/canvas.py b/src/helpers/canvas.py new file mode 100644 index 0000000..1c69420 --- /dev/null +++ b/src/helpers/canvas.py @@ -0,0 +1,103 @@ +import cv2 +import tkinter +from PIL import ImageTk, Image +import datetime +import pathlib +import os + +from helpers.logger import C_INFO, C_DONE, C_ERR + +class CVSuiteCanvas: + def __init__(self, canvas): + self.canvas = canvas + + self.name = "" # Name of current image + self.imgs = [] # OpenCV Image data + self.tk_imgs = [] # Tkinter Image data + self.tags = [] # Tag data + + def add(self, data, name: str): + """ + Add CV2 image to canvas output + """ + self.imgs.append(data) + self.tags.append(name) + + def clear(self): + """ + Clear canvas + """ + self.imgs = [] + self.tags = [] + + def draw(self, size): + """ + Update canvas + """ + # Check if size of canvas has updated + drawW = self.canvas.winfo_width() + + # Reset drawing position + drawX = 0 + drawY = 0 + + # Clear previously printed images + self.tk_imgs = [] + + # self.meta.config(state=tkinter.NORMAL) + # self.meta.delete(1.0, tkinter.END) + # self.meta.insert(tkinter.END, f"{self.img_name}\n") + + # Draw all output images + for idx, data in enumerate(self.imgs): + # Create ui image + tk_img = cv2.cvtColor(data, cv2.COLOR_BGR2RGB) + tk_img = ImageTk.PhotoImage(image=Image.fromarray(tk_img)) + self.tk_imgs.append(tk_img) + + ## Check if next item will be out of range + if drawX + size >= drawW: + drawY += size + drawX = 0 + self.canvas.configure(height=(drawY + size)) + + self.canvas.create_image( + drawX, drawY, anchor=tkinter.NW, image=self.tk_imgs[idx], tags="og" + ) + + # Add name to text box + tag = self.canvas.create_text(drawX+5, drawY+5, anchor=tkinter.NW, text=F"{idx} {self.tags[idx]}", fill="black", font=('Helvetica 12 bold')) + tag_bg = self.canvas.create_rectangle(self.canvas.bbox(tag), fill="white") + self.canvas.tag_lower(tag_bg,tag) + # self.meta.insert(tkinter.END, f"{idx}: {self.output[1][idx]}\n") + + drawX += size + + # Clear output + # self.meta.config(state=tkinter.DISABLED) + + def export(self, id, name, path): + """ + Export id to file + """ + # Get export settings + img_arr = self.tk_imgs + + print(C_INFO, f"Using path: {path}") + + if id >= 0 and id < len(img_arr): + # Create file + now = datetime.datetime.now() + new_file_name = f"{name}-{self.tags[id]}-{now.strftime('%Y-%m-%dT%H.%M.%S')}.png" + + # Put data + file_path = pathlib.Path(path, new_file_name) + # print(file_path) + + img_pil = ImageTk.getimage(self.tk_imgs[id]) + img_pil.save(file_path, "PNG") + img_pil.close() + + print(C_DONE, f"Exported Image ID {id} ({self.tags[id]}) to {os.path.join(path, new_file_name)}") + else: + print(C_ERR, "Nothing to export!") \ No newline at end of file diff --git a/src/helpers/gui/main.ui b/src/helpers/gui/main.ui index abd321f..0f899d7 100644 --- a/src/helpers/gui/main.ui +++ b/src/helpers/gui/main.ui @@ -194,8 +194,8 @@ + imgExport Export PNG - 1 1 @@ -225,25 +225,6 @@ - - - 15 - 0 - 4 - false - normal - true - Image IDs should appear here - 25 - word - - 4 - 1 - 1 - 7 - - - bottom @@ -281,7 +262,7 @@ - applyAll + imgCycle Export ID for entire dataset 2 @@ -297,14 +278,14 @@ Powered by ARNweb.nl & TomSelier.com 4 - 2 + 1 8 - applyAll + imgCycle Run analysis for entire dataset (!) 2 @@ -318,7 +299,7 @@ imgCtl - < Prev tree + < Prev tag 2 10 @@ -330,7 +311,7 @@ imgCtl - Next tree > + Next tag > 3 10 @@ -347,27 +328,18 @@ 25 word - 5 + 4 1 1 7 - - - Metadata - - 4 - 0 - - - Test results - 5 + 4 0 diff --git a/src/helpers/logger.py b/src/helpers/logger.py index fd8c63f..521873b 100644 --- a/src/helpers/logger.py +++ b/src/helpers/logger.py @@ -1,15 +1,22 @@ import pathlib import datetime +# Some feedback with colours to make console output easier to read +C_INFO = "\u001b[96m*INFO*\u001b[0m\t" # Info - Cyan +C_WARN = "\u001b[33m*WARN*\u001b[0m\t" # Warning - Orange +C_ERR = "\u001b[31m*ERR*\u001b[0m\t" # Error - Red +C_USER = "\u001b[35m*USER*\u001b[0m\t" # User input - Purple +C_DONE = "\u001b[32m*DONE*\u001b[0m\t" # Done - Green +C_DBUG = "\u001b[94m*DBUG*\u001b[0m\t" # Debug - Blue + now = datetime.datetime.now() - -class Logger: +class CVSuiteLogger: def __init__(self, path): self.fileName = pathlib.Path( path, f"result-{now.strftime('%Y-%m-%dT%H.%M.%S')}.csv" ) - self.file = open(self.fileName, "x") + self.file = open(self.fileName, encoding="utf-8", mode="x") self.first = True self.index = [] @@ -19,15 +26,15 @@ class Logger: self.index.append(name) self.data.append(value) - def csv(self, input): + def csv(self, data): result = "" - for idx, item in enumerate(input): + for idx, item in enumerate(data): result += str(item) - if idx != (len(input) - 1): + if idx != (len(data) - 1): result += ", " result += "\n" - print(result) + # print(result) return result diff --git a/src/helpers/treenum.py b/src/helpers/tags.py similarity index 100% rename from src/helpers/treenum.py rename to src/helpers/tags.py diff --git a/src/suite.py b/src/suite.py index 8555170..8effa38 100644 --- a/src/suite.py +++ b/src/suite.py @@ -1,48 +1,48 @@ #!/usr/bin/python3 import pathlib -import pygubu import glob -import tkinter -from PIL import ImageTk, Image -import numpy as np -import cv2 -import time -import matplotlib.pyplot as plt import json import datetime import os import copy +from io import open +import numpy as np +import cv2 +# GUI +import pygubu +import matplotlib.pyplot as plt + +# Helpers from helpers.statistics import imgStats -from helpers.logger import Logger +from helpers.logger import CVSuiteLogger, C_DBUG +from helpers.canvas import CVSuiteCanvas ## UI config load PROJECT_PATH = pathlib.Path(__file__).parent PROJECT_UI = "./src/helpers/gui/main.ui" +TITLE = "Tree Recogniser 7000" ## Config file load CONFIG_PATH = "./src/config/config.json" -config_file = open(CONFIG_PATH) +config_file = open(CONFIG_PATH, encoding="utf-8") config_json = json.load(config_file) -log = Logger(config_json["out"]) - - ## UI class setup -class MainApp: +class CVSuite: def __init__(self, master=None): self.builder = builder = pygubu.Builder() builder.add_resource_path(PROJECT_PATH) builder.add_from_file(PROJECT_UI) - # Main widget + # Main window self.mainwindow = builder.get_object("main", master) # Canvas for output images - self.canvas = builder.get_object("output_canvas") - self.tk_imgs = [] # Required or python will forget - self.meta = builder.get_object("dataset") - self.output = [[] for x in range(2)] + self.canvas = CVSuiteCanvas(builder.get_object("output_canvas")) + + # Log file + self.log = CVSuiteLogger(config_json["out"]["log"]) # Keep track of images in dataset self.img_current = 0 @@ -90,7 +90,7 @@ class MainApp: """ if plt is not None: plt.close() # Close graph vies - log.file.close() # Close log files + self.log.file.close() # Close log files self.mainwindow.quit() # Close main @@ -108,19 +108,19 @@ class MainApp: # Determine detection based on widget id if cmd[0] == "next": - dir = 1 + cdir = 1 elif cmd[0] == "prev": - dir = -1 + cdir = -1 # Get name of current img start = copy.deepcopy( self.img_name.split("_")[0] ) # deepcopy cus snaky boi language likes to create pointers - next = start + inext = start - while start == next: + while start == inext: # Check for limits - self.img_current += dir + self.img_current += cdir if self.img_current == self.img_max: self.img_current = 0 elif self.img_current == -1: @@ -130,43 +130,22 @@ class MainApp: break # Stop if only one image should be skipped elif cmd[1] == "tree": self.updatePath() - next = copy.deepcopy(self.img_name.split("_")[0]) + inext = copy.deepcopy(self.img_name.split("_")[0]) # Update UI self.update(self) - def apply(self, event=None, path=None): + def imgExport(self, event=None, path=config_json["out"]["img"]): """ - Export current dataset + Export given preprocess id to file """ - # Get export settings - img_arr = self.tk_imgs - img_id = self.export_id.get() - if path == None: - path = config_json["out"] - else: - print(f"Using path: {path}") + iid = self.export_id.get() + self.canvas.export(iid, self.img_name.split("_")[0], path) - if img_id >= 0 and img_id < len(img_arr): - # Create file - now = datetime.datetime.now() - new_file_name = f"{self.img_current}-{self.output[1][img_id]}-{now.strftime('%Y-%m-%dT%H.%M.%S')}.png" - - # Put data - file_path = pathlib.Path(path, new_file_name) - # print(file_path) - - imgpil = ImageTk.getimage(self.tk_imgs[img_id]) - imgpil.save(file_path, "PNG") - imgpil.close() - - print(f"Exported Image ID {img_id} to {os.path.join(path, new_file_name)}") - else: - print("Nothing to export!") - - def applyAll(self, widget_id): + def imgCycle(self, widget_id): """ - Export given preprocess id for every image in the dataset folder + GUI Button callback + Cycle through all images in the data set """ if widget_id == "export": export = True @@ -179,14 +158,14 @@ class MainApp: if export: now = datetime.datetime.now() path = pathlib.Path( - config_json["out"], - f"{self.output[1][img_id]}-all-{now.strftime('%Y-%m-%dT%H.%M.%S')}/", + config_json["out"]["img"], + f"{self.canvas.tags[img_id]}-all-{now.strftime('%Y-%m-%dT%H.%M.%S')}/", ) os.mkdir(path) while True: if export: - self.apply(path=path) + self.imgExport(path=path) self.imgCtl("next_img") if self.img_current == img_current: @@ -195,52 +174,6 @@ class MainApp: ## Ensure display is always correct with image self.update() - def addOutput(self, data, name: str): - """ - Add CV2 image to canvas output - """ - self.output[0].append(data) - self.output[1].append(name) - - def drawOutput(self, size): - # Check if size of canvas has updated - drawW = self.canvas.winfo_width() - - # Reset drawing position - drawX = 0 - drawY = 0 - - # Clear previously printed images - self.tk_imgs = [] - - self.meta.config(state=tkinter.NORMAL) - self.meta.delete(1.0, tkinter.END) - self.meta.insert(tkinter.END, f"{self.img_name}\n") - - # Draw all output images - for idx, data in enumerate(self.output[0]): - # Create ui image - tk_img = cv2.cvtColor(data, cv2.COLOR_BGR2RGB) - tk_img = ImageTk.PhotoImage(image=Image.fromarray(tk_img)) - self.tk_imgs.append(tk_img) - - ## Check if next item will be out of range - if drawX + size >= drawW: - drawY += size - drawX = 0 - self.canvas.configure(height=(drawY + size)) - - self.canvas.create_image( - drawX, drawY, anchor=tkinter.NW, image=self.tk_imgs[idx], tags="og" - ) - drawX += size - - # Add name to text box - self.meta.insert(tkinter.END, f"{idx}: {self.output[1][idx]}\n") - - # Clear output - self.meta.config(state=tkinter.DISABLED) - def createPlot(self, columns, rows): fig, axs = plt.subplots(columns, rows) return axs @@ -282,9 +215,9 @@ class MainApp: func = np.diag(results) diff = np.diff(func) area = sum(func) - + self.axs[column, row - 1].clear() - self.axs[column, row - 1].title.set_text(F"Area: {area}") + self.axs[column, row - 1].title.set_text(f"Area: {area}") self.axs[column, row - 1].plot(func) self.axs[column, row - 1].plot(diff) @@ -299,15 +232,15 @@ class MainApp: lambda x, pos: str(x * canny_step) ) - log.add("Canny Mean", func.mean()) - log.add("Canny Std", func.std()) - log.add("Canny Min", func.min()) - log.add("Canny Max x", np.where(func==func.max())[0][0]) - log.add("Canny Max y", func.max()) - log.add("Canny Diff max y", diff.max()) - log.add("Canny Diff min x",np.where(diff==diff.min())[0][0]) - log.add("Canny Diff min y", diff.min()) - log.add("Canny Area", area) + self.log.add("Canny Mean", func.mean()) + self.log.add("Canny Std", func.std()) + self.log.add("Canny Min", func.min()) + self.log.add("Canny Max x", np.where(func == func.max())[0][0]) + self.log.add("Canny Max y", func.max()) + self.log.add("Canny Diff max y", diff.max()) + self.log.add("Canny Diff min x", np.where(diff == diff.min())[0][0]) + self.log.add("Canny Diff min y", diff.min()) + self.log.add("Canny Area", area) def writeStats(self, img, labels, column, row): mean, std = imgStats(img) @@ -330,8 +263,8 @@ class MainApp: ) for idx, label in enumerate(labels): - log.add(f"Mean {label}", mean[idx]) - log.add(f"Std {label}", std[idx]) + self.log.add(f"Mean {label}", mean[idx]) + self.log.add(f"Std {label}", std[idx]) def updatePath(self): """ @@ -357,7 +290,7 @@ class MainApp: def update(self, event=None, part_update=None): ## Check if hist and canny hm have to be rerendered if ( - part_update == None + part_update is None ): ## If partial update has not been forced, check if full update is required if self.img_current != self.img_old or self.img_size != self.img_size_old: part_update = False @@ -372,7 +305,10 @@ class MainApp: print("Full update forced!") if self.updatePath(): - log.add("Tree", self.img_name.split("_")[0]) + print(C_DBUG, F"Processing {self.img_name}") + + self.mainwindow.title(F"{TITLE} - {self.img_name}") + self.log.add("Tree", self.img_name.split("_")[0]) # Get all user vars ct1 = self.canny_thr1.get() @@ -383,17 +319,17 @@ class MainApp: bright = self.brightness.get() # Clear output - self.output = [[] for x in range(2)] + self.canvas.clear() # Import and resize image # img = cv2.imread(images[self.img_current]) img = cv2.imread(os.path.join(self.img_path.get(), self.img_name)) - # img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA) - self.addOutput(img, "Original") + img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA) + self.canvas.add(img, "Original") # Set grayscale img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - self.addOutput(img_gray, "Grayscale") + self.canvas.add(img_gray, "Grayscale") # Contrast / brightness boost contrast_val = contrast / 100 @@ -401,12 +337,12 @@ class MainApp: img_contrast = np.clip( contrast_val * (img_gray + bright_val), 0, 255 ).astype(np.uint8) - # self.addOutput(img_contrast, F"Contrast / Brightness\n c+{contrast_val} b+{bright_val}") - self.addOutput(img_contrast, f"BCG") + # self.canvas.add(img_contrast, F"Contrast / Brightness\n c+{contrast_val} b+{bright_val}") + self.canvas.add(img_contrast, f"BCG") # Blurred edition img_blur = cv2.GaussianBlur(img_gray, (3, 3), 0) - self.addOutput(img_blur, "Blurred_k3") + self.canvas.add(img_blur, "Blurred_k3") # Sobel edge if sxy in ["x", "y", "both"]: @@ -426,17 +362,17 @@ class MainApp: else: img_sobel = img_gray - self.addOutput(img_sobel, "Sobel_edge") - log.add("Sobel nonzero", cv2.countNonZero(img_sobel)) + self.canvas.add(img_sobel, "Sobel_edge") + self.log.add("Sobel nonzero", cv2.countNonZero(img_sobel)) # Canny edge img_canny = cv2.Canny(image=img_blur, threshold1=ct1, threshold2=ct2) - self.addOutput(img_canny, "Canny_edge") + self.canvas.add(img_canny, "Canny_edge") # BGR - self.addOutput(img[:, :, 0], "BGR_B") - self.addOutput(img[:, :, 1], "BGR_G") - self.addOutput(img[:, :, 2], "BGR_R") + self.canvas.add(img[:, :, 0], "BGR_B") + self.canvas.add(img[:, :, 1], "BGR_G") + self.canvas.add(img[:, :, 2], "BGR_R") if img is not None: self.drawHist(img, ("B", "G", "R"), 0, 0) @@ -444,10 +380,10 @@ class MainApp: # HSV img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) - self.addOutput(img_hsv, "HSV") - self.addOutput(img_hsv[:, :, 0], "HSV_H") # H - self.addOutput(img_hsv[:, :, 1], "HSV_S") # S - self.addOutput(img_hsv[:, :, 2], "HSV_V") # V + self.canvas.add(img_hsv, "HSV") + self.canvas.add(img_hsv[:, :, 0], "HSV_H") # H + self.canvas.add(img_hsv[:, :, 1], "HSV_S") # S + self.canvas.add(img_hsv[:, :, 2], "HSV_V") # V if not part_update: if img_hsv is not None: @@ -460,14 +396,15 @@ class MainApp: # Write results to CSV file if not part_update: - log.update() + self.log.update() else: - log.clear() # Prevent partial updates from breaking log + self.log.clear() # Prevent partial updates from breaking log # Show all data plt.show(block=False) ## Graphs - self.drawOutput(size) ## Images + self.canvas.draw(size) ## Images + if __name__ == "__main__": - app = MainApp() + app = CVSuite() app.run()