This commit is contained in:
Tom Selier 2023-10-13 20:10:22 +02:00
commit 311070d1a6
8 changed files with 205 additions and 183 deletions

BIN
.pylintrc Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,8 @@
{
"path": "",
"out": "",
"size": 750
"out": {
"img": "",
"log": ""
},
"size": 100
}

103
src/helpers/canvas.py Normal file
View File

@ -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!")

View File

@ -194,8 +194,8 @@
</child>
<child>
<object class="ttk.Button" id="button3">
<property name="command" type="command" cbtype="simple">imgExport</property>
<property name="text" translatable="yes">Export PNG</property>
<bind sequence="&lt;ButtonPress&gt;" handler="apply" add="" />
<layout manager="grid">
<property name="column">1</property>
<property name="columnspan">1</property>
@ -225,25 +225,6 @@
</layout>
</object>
</child>
<child>
<object class="tk.Text" id="dataset" named="True">
<property name="height">15</property>
<property name="pady">0</property>
<property name="selectborderwidth">4</property>
<property name="setgrid">false</property>
<property name="state">normal</property>
<property name="takefocus">true</property>
<property name="text" translatable="yes">Image IDs should appear here</property>
<property name="width">25</property>
<property name="wrap">word</property>
<layout manager="grid">
<property name="column">4</property>
<property name="columnspan">1</property>
<property name="row">1</property>
<property name="rowspan">7</property>
</layout>
</object>
</child>
<child>
<object class="ttk.LabeledScale" id="s_bright" named="True">
<property name="compound">bottom</property>
@ -281,7 +262,7 @@
</child>
<child>
<object class="ttk.Button" id="export" named="True">
<property name="command" type="command" cbtype="with_wid">applyAll</property>
<property name="command" type="command" cbtype="with_wid">imgCycle</property>
<property name="text" translatable="yes">Export ID for entire dataset</property>
<layout manager="grid">
<property name="column">2</property>
@ -297,14 +278,14 @@
<property name="text" translatable="yes">Powered by ARNweb.nl &amp; TomSelier.com</property>
<layout manager="grid">
<property name="column">4</property>
<property name="columnspan">2</property>
<property name="columnspan">1</property>
<property name="row">8</property>
</layout>
</object>
</child>
<child>
<object class="ttk.Button" id="analyse" named="True">
<property name="command" type="command" cbtype="with_wid">applyAll</property>
<property name="command" type="command" cbtype="with_wid">imgCycle</property>
<property name="text" translatable="yes">Run analysis for entire dataset (!)</property>
<layout manager="grid">
<property name="column">2</property>
@ -318,7 +299,7 @@
<child>
<object class="ttk.Button" id="prev_tree" named="True">
<property name="command" type="command" cbtype="with_wid">imgCtl</property>
<property name="text" translatable="yes">&lt; Prev tree</property>
<property name="text" translatable="yes">&lt; Prev tag</property>
<layout manager="grid">
<property name="column">2</property>
<property name="ipadx">10</property>
@ -330,7 +311,7 @@
<child>
<object class="ttk.Button" id="next_tree" named="True">
<property name="command" type="command" cbtype="with_wid">imgCtl</property>
<property name="text" translatable="yes">Next tree &gt;</property>
<property name="text" translatable="yes">Next tag &gt;</property>
<layout manager="grid">
<property name="column">3</property>
<property name="ipadx">10</property>
@ -347,27 +328,18 @@
<property name="width">25</property>
<property name="wrap">word</property>
<layout manager="grid">
<property name="column">5</property>
<property name="column">4</property>
<property name="columnspan">1</property>
<property name="row">1</property>
<property name="rowspan">7</property>
</layout>
</object>
</child>
<child>
<object class="ttk.Label" id="l_meta" named="True">
<property name="text" translatable="yes">Metadata</property>
<layout manager="grid">
<property name="column">4</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="ttk.Label" id="l_tests" named="True">
<property name="text" translatable="yes">Test results</property>
<layout manager="grid">
<property name="column">5</property>
<property name="column">4</property>
<property name="row">0</property>
</layout>
</object>

View File

@ -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

View File

@ -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()