Initial commit
This commit is contained in:
commit
e03409fcd4
10 changed files with 472 additions and 0 deletions
41
README.md
Executable file
41
README.md
Executable 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
0
Storage/.gitkeep
Normal file
15
classes.txt
Normal file
15
classes.txt
Normal 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
16
environment.yml
Normal 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
187
src/boundingBox.py
Normal 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
46
src/imageHandler.py
Normal 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
57
src/imageViewer.py
Normal 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
5
src/main.py
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
from window import Window
|
||||
|
||||
if __name__ == "__main__":
|
||||
window = Window()
|
||||
window.mainloop()
|
||||
49
src/settings.py
Normal file
49
src/settings.py
Normal 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
56
src/window.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue