Initial commit

This commit is contained in:
qkinader 2022-12-28 22:30:19 +01:00
commit e03409fcd4
10 changed files with 472 additions and 0 deletions

41
README.md Executable file
View file

@ -0,0 +1,41 @@
# Real Video Labeling Tool Py
Python rewrite of [Real-Video-Annotation-Tool]
## 1. Installation
Here are two options to setup the project.
### 1.1.1 Setup with Conda
1. If [conda] is not yet installed, follow the [installation guide] for your operating system.
> Note: [miniconda] and especially [mamba] are faster and lighter than Anaconda
2. From the projects root directory create the environment with:
```
conda env create -f environment.yml
```
3. Activate the new environment with:
```
conda activate labeling_tool
```
> Note: The environment should always be activated when working on this project
4. To update the environment after changes in environment.yml run:
```conda env update --name labeling_tool --file environment.yml --prune```
#### Troubleshoot
- Make sure to restart your terminal after installing conda. Or run ```source ~/.bashrc```
- Run ```conda config --set auto_activate_base false``` to deactivate automatic conda activation on startup
- If ```conda activate``` fails, try ```conda init``` and then ```conda activate``` again
### 1.1.2 Install dependencies with pip
You can also install dependencies with pip:
```pip install -r requirements.txt```
### 1.2 Run
Run program with ```python3 src/main.py``` or from within IDE.
Images and Labels are stored in /Storage
## Note
[Real-Video-Annotation-Tool]: https://sam-dev.cs.hm.edu/SAM-DEV/Sampler_CNN_Training
[conda]: https://docs.conda.io/
[installation guide]: https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html
[miniconda]: https://docs.conda.io/en/latest/miniconda.html
[mamba]: https://mamba.readthedocs.io/en/latest/installation.html#installation

0
Storage/.gitkeep Normal file
View file

15
classes.txt Normal file
View file

@ -0,0 +1,15 @@
person
child
car
emergency
trafficlight
trafficlight_green
trafficlight_yellow
trafficlight_red
pit_in
pit_out
park_parallel
park_cross
overtaking_prohibited
overtaking_permitted
traffic_sign

16
environment.yml Normal file
View file

@ -0,0 +1,16 @@
name: labeling_tool
channels:
- conda-forge
- nodefaults
dependencies:
- python==3.9
- pip
- numpy==1.23.5
- tk==8.6.12
- pip:
- ttkbootstrap==1.10.0
- opencv-contrib-python-headless==4.6.0.66
- pillow==9.3.0
- natsort==8.2.0

187
src/boundingBox.py Normal file
View file

@ -0,0 +1,187 @@
import cv2
import random
import ttkbootstrap as ttk
from tkinter import StringVar, Frame, Label
def get_color() -> str:
de = ("%02x" % random.randint(0, 255))
re = ("%02x" % random.randint(0, 255))
we = ("%02x" % random.randint(0, 255))
color = "#" + de + re + we
return color
class BoundingBox:
bboxes = []
current = None
counter = 0
current_index = 0
classes = [(i.strip()) for i in open('classes.txt', 'r').readlines()]
classes = dict(zip(classes, range(0, len(classes) + 1)))
tracker = cv2.TrackerCSRT_create()
@classmethod
def new_label(cls, canvas, frm_buttons):
BoundingBox(canvas, frm_buttons)
cls.current_index = len(cls.bboxes)-1
cls.current = cls.bboxes[cls.current_index]
@classmethod
def switch_box(cls, _):
try:
cls.current_index = (cls.current_index + 1) % len(cls.bboxes)
cls.current = cls.bboxes[cls.current_index]
cls.current.__set_current()
except ZeroDivisionError:
pass
@classmethod
def move_current(cls, event):
cls.current.move(event)
@classmethod
def motion_current(cls, event):
cls.current.motion(event)
@classmethod
def track_boxes(cls, images):
tracker = cv2.TrackerCSRT_create()
for box in cls.bboxes:
bbox = (box.start_x, box.start_y, box.x - box.start_x, box.y - box.start_y)
tracker.init(images.previous_image, bbox)
ok, bbox = tracker.update(images.images[images.current_index])
box.start_x = bbox[0]
box.start_y = bbox[1]
box.x = box.start_x + bbox[2]
box.y = box.start_y + bbox[3]
box.update_coords()
@classmethod
def write_labels(cls, file):
filename = str(file)
filename = filename.replace(".jpg", ".txt")
with open(filename, "w") as file:
for box in cls.bboxes:
width = box.x - box.start_x
height = box.y - box.start_y
box_properties = [cls.classes[box.lbl_class], box.start_x, box.start_y, width, height]
file.writelines(", ".join(map(str, box_properties)))
file.write("\n")
def __init__(self, canvas, frm_buttons):
self.canvas = canvas
self.frm_buttons = frm_buttons
BoundingBox.counter += 1
self.lbl_class: str = next(iter(BoundingBox.classes))
self.color: str = get_color()
# Coordinates
self.start_x = 0
self.start_y = 0
self.x = 50
self.y = 50
# Draw initial Rectangle with Label text
self.rect = self.canvas.create_rectangle(self.start_x, self.start_y, self.x, self.y,
outline=self.color, width=5)
self.text = self.canvas.create_text(self.start_x, self.start_y - 20,
text=f"{BoundingBox.counter}-{self.lbl_class}",
fill=self.color, font=('Arial', 30), anchor='w')
# Create Box Selection Frame with Buttons (Select, Remove etc.) for Bounding Box
self.lbl_frame = Frame(self.frm_buttons, bg=self.color, width=200, height=80)
self.lbl_text = Label(self.lbl_frame, text=f"{BoundingBox.counter}-{self.lbl_class}", font=("Arial", 12))
self.lbl_frame.bind("<Button-1>", self.__set_current_click)
self.__setup_box_buttons()
self.__set_current()
BoundingBox.bboxes.append(self)
def move(self, event, motion=False):
if motion:
self.x, self.y = (event.x, event.y)
elif event.num == 1:
self.start_x = event.x
self.start_y = event.y
return
elif event.keysym == "a":
self.start_x -= 1 # left
elif event.keysym == "s":
self.start_y += 1 # down
elif event.keysym == "w":
self.start_y -= 1 # up
elif event.keysym == "d":
self.start_x += 1 # right
elif event.keysym == "j":
self.x -= 1 # left
elif event.keysym == "k":
self.y += 1 # down
elif event.keysym == "i":
self.y -= 1 # up
elif event.keysym == "l":
self.x += 1 # right
self.update_coords()
def motion(self, event):
self.move(event, motion=True)
def update_coords(self):
self.canvas.coords(self.rect, self.start_x, self.start_y, self.x, self.y)
self.canvas.coords(self.text, self.start_x, self.start_y-20)
def __setup_box_buttons(self):
self.lbl_frame.grid(pady=0, padx=2, sticky='ew')
self.lbl_frame.grid_propagate(True)
# Bounding Box Title as number of box and box class
self.lbl_text.grid(row=0, column=0, columnspan=3, sticky='w')
# Class Selection Dropdown
variable = StringVar(self.frm_buttons, self.lbl_class)
lbl_class_om = ttk.OptionMenu(self.lbl_frame, variable, self.lbl_class, *BoundingBox.classes.keys(),
command=self.change_class)
lbl_class_om.grid(row=1, column=0, pady=10, columnspan=3)
# Buttons (Select, New Color and Remove)
select_btn = ttk.Button(self.lbl_frame, text='Select', command=self.__set_current)
select_btn.grid(row=2, column=0, pady=10)
new_color = ttk.Button(self.lbl_frame, text='New Color', command=self.new_color)
new_color.grid(row=2, column=1, pady=10)
rm_btn = ttk.Button(self.lbl_frame, text='Remove', command=self.remove)
rm_btn.grid(row=2, column=2, pady=10)
self.new_color()
def change_class(self, event):
self.lbl_class = event
self.lbl_text.configure(text=f"{BoundingBox.counter}-{self.lbl_class}") # TODO fix counter when changing class
self.canvas.itemconfig(self.text, text=f"{BoundingBox.counter}-{self.lbl_class}")
def __set_current_click(self, _):
self.__set_current()
def __set_current(self):
BoundingBox.current = self
for box in BoundingBox.bboxes:
box.lbl_frame.configure(highlightthickness=0)
box.canvas.itemconfigure(box.rect, width=3)
self.canvas.itemconfigure(self.rect, width=5)
self.lbl_frame.configure(highlightbackground="white", highlightcolor="white", highlightthickness=6)
def new_color(self):
self.color = get_color()
self.canvas.itemconfig(self.rect, outline=self.color)
self.canvas.itemconfig(self.text, fill=self.color)
self.lbl_frame.configure(background=self.color, highlightbackground="white", highlightcolor="white")
def remove(self):
self.lbl_frame.destroy()
self.canvas.delete(self.rect, self.text)
BoundingBox.bboxes = [box for box in BoundingBox.bboxes if box != self]

46
src/imageHandler.py Normal file
View file

@ -0,0 +1,46 @@
import cv2
import pathlib
from tkinter.filedialog import askopenfilename
from natsort import natsorted
IMAGE_PATH = "Storage/" # TODO: Windows??
class ImageHandler:
def __init__(self):
self.filepath = askopenfilename(filetypes=[("Text Files", "*.mp4"), ("All Files", "*.*")])
self.image_path = IMAGE_PATH
self.video = cv2.VideoCapture(self.filepath)
# Read Video with opencv
success, self.current_cv_read = self.video.read()
self.current_index = 0
self.images = [self.current_cv_read]
self.previous_image = self.images[self.current_index]
# Read first frame
self.write_img()
self.current_img = pathlib.Path(self.get_file_list()[self.current_index])
def write_img(self):
cv2.imwrite(f"{self.image_path}frame%d.jpg" % self.current_index, self.current_cv_read)
def get_file_list(self):
return natsorted(list(pathlib.Path(self.image_path).glob("*.jpg")))
def next(self):
self.current_index += 1
# If we want to read a new image
if self.current_index == len(self.images):
success, self.current_cv_read = self.video.read()
self.write_img()
self.images.append(self.current_cv_read)
self.current_img = pathlib.Path(self.get_file_list()[self.current_index])
self.previous_image = self.images[self.current_index - 1]
def previous(self):
if self.current_index > 0:
self.current_index -= 1
self.previous_image = self.images[self.current_index-1]
self.current_img = pathlib.Path(self.get_file_list()[self.current_index])

57
src/imageViewer.py Normal file
View file

@ -0,0 +1,57 @@
import ttkbootstrap as ttk
from PIL import ImageTk, Image
import cv2
from boundingBox import BoundingBox
from imageHandler import ImageHandler
class ImageViewer(ttk.Canvas):
def __init__(self, window, frame):
# Initialize and create Tkinter Canvas
super().__init__(frame, width=window.winfo_width(), height=window.winfo_height(), cursor="cross")
self.pack(side="top", expand=True)
# Create boxes and Image Handler
self.image = ImageHandler()
self.displayed_img = self.__get_current_photo_image()
self.image_container = self.create_image(0, 0, anchor="nw", image=self.displayed_img)
self.display_img()
# Keyboard Bindings
self.focus_set()
window.bind("<Right>", self.forward)
window.bind("<Left>", self.backward)
self.bind("<Shift_L>", BoundingBox.switch_box)
self.bind("<Button-1>", BoundingBox.move_current)
self.bind("<B1-Motion>", BoundingBox.motion_current)
self.bind("a", BoundingBox.move_current)
self.bind("s", BoundingBox.move_current)
self.bind("w", BoundingBox.move_current)
self.bind("d", BoundingBox.move_current)
self.bind("j", BoundingBox.move_current)
self.bind("k", BoundingBox.move_current)
self.bind("i", BoundingBox.move_current)
self.bind("l", BoundingBox.move_current)
def __get_current_photo_image(self) -> ImageTk.PhotoImage:
rgb_image = cv2.cvtColor(self.image.current_cv_read, cv2.COLOR_BGR2RGB)
img = Image.fromarray(rgb_image)
return ImageTk.PhotoImage(img)
def display_img(self):
# Opencv reads frames as bgr image, we need to convert it into rgb to display it correctly
rgb_image = cv2.cvtColor(self.image.images[self.image.current_index], cv2.COLOR_BGR2RGB)
img = Image.fromarray(rgb_image)
self.displayed_img = ImageTk.PhotoImage(img)
self.itemconfig(self.image_container, image=self.displayed_img)
def forward(self, _):
BoundingBox.write_labels(file=self.image.current_img)
self.image.next()
self.display_img()
BoundingBox.track_boxes(self.image)
def backward(self, _):
self.image.previous()
self.display_img()

5
src/main.py Executable file
View file

@ -0,0 +1,5 @@
from window import Window
if __name__ == "__main__":
window = Window()
window.mainloop()

49
src/settings.py Normal file
View file

@ -0,0 +1,49 @@
import cv2
import ttkbootstrap as ttk
from tkinter import Toplevel, Frame, Label, IntVar
from boundingBox import BoundingBox
"""
WIP
"""
class Settings(Toplevel):
def __init__(self, master=None):
Toplevel.__init__(self, master)
self.wm_title("Settings")
self.rowconfigure(0, minsize=480, weight=1)
self.columnconfigure(0, minsize=640, weight=1)
self.grab_set()
self.frame = Frame(self, padx=80, pady=40)
self.frame.pack(side='top')
# Tracker type options
tracker_type_txt = Label(self.frame, text="Tracker Type (WIP)", font=('Arial', 14), pady=20)
tracker_type_txt.pack()
self.trackers = {"CSRT": 1,
"GOTURN": 2, # TODO
"KCF": 3} # TODO
self.v2 = IntVar(self.frame, 1)
self.v2.trace_add('write', self.set_tracker)
for (text, value) in self.trackers.items():
ttk.Radiobutton(self.frame, text=text, variable=self.v2, value=value).pack(side='top', anchor='w')
# Label Revision Options
label_revision_txt = Label(self.frame, text="Label Revision (WIP)", font=('Arial', 14), pady=20)
label_revision_txt.pack()
revision_types = {"Overwrite Files (Tracker active)": 1,
"Add (Tracker only for new Boxes)": 2, # TODO
"Adjust (No Tracker)": 3} # TODO
v = IntVar(self.frame, 1)
for (text, value) in revision_types.items():
ttk.Radiobutton(self.frame, text=text, variable=v, value=value).pack(side='top', anchor='w')
def set_tracker(self, _):
cv_trackers = {1: cv2.TrackerCSRT_create(),
2: cv2.TrackerGOTURN_create(),
3: cv2.TrackerKCF_create()}
BoundingBox.tracker = cv_trackers[self.v2.get()]
print(f"Tracker type changed to {BoundingBox.tracker}")

56
src/window.py Normal file
View file

@ -0,0 +1,56 @@
import tkinter as tk
from tkinter import Menu
import ttkbootstrap as ttk
from imageViewer import ImageViewer
from settings import Settings
from boundingBox import BoundingBox
IMAGE_SIZE_X = 960
IMAGE_SIZE_Y = 1280
class Window(ttk.Window):
def __init__(self):
super().__init__(themename="superhero")
# Set window properties
self.wm_title("Labeling Tool")
self.rowconfigure(0, minsize=IMAGE_SIZE_X, weight=1)
self.columnconfigure(0, minsize=IMAGE_SIZE_Y, weight=1)
# Set Menu Bar for Settings and opening Video file
self.__menu_bar()
# Sidebar (Frame) for Buttons and BoundingBox Selection
frm_buttons = tk.Frame(self, relief=tk.RAISED, bd=1)
frm_buttons.grid(row=0, column=2, sticky="ns")
# Image Frame to display Image as Canvas
frm_image = tk.Frame(self, relief=tk.RAISED)
frm_image.grid(row=0, column=0, sticky="ew")
self.update()
# Load Canvas into image Frame to display Video frame and draw BBoxes
image_view = ImageViewer(self, frm_image)
# Button for adding Bounding Boxes
btn_add_class = tk.Button(frm_buttons, text="Add Class",
command=lambda: BoundingBox.new_label(image_view, frm_buttons))
btn_add_class.grid(row=0, column=0, sticky="nwe", padx=5, pady=5)
def __menu_bar(self):
menu = Menu()
self.config(menu=menu)
# File Opener Menu
file_menu = Menu(menu)
menu.add_cascade(label='File', menu=file_menu)
# file_menu.add_command(label='Open...', command=Fileloader.open_video)
# Settings Menu
settings = Menu(menu)
menu.add_cascade(label='Settings', menu=settings)
settings.add_command(label='Settings', command=self.__open_settings)
def __open_settings(self):
Settings(self)