Skip to content
This repository was archived by the owner on Apr 24, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
678 changes: 548 additions & 130 deletions MousePositionTracker.py

Large diffs are not rendered by default.

34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ $ conda env create -f ROI_ENV.yml
```
The main one is that it requires pandas to not be on version 1.x. We use `pandas=0.25.3` :).


![ROI](static/ROI.jpg)
![ROI](static/ROI_tool.jpg)


## Launching the GUI
Expand All @@ -32,7 +31,7 @@ $ python ROI_tool.py
When you initially launch the application, you will be asked to choose a video on which to draw your ROI.

### Step 2. Draw Your ROIs
Click and drag to draw your ROIs and save them sequentially with `Set and Name.`
Click and drag to draw your ROIs (you can select the shape, rectangle or oval, as well as the dimension in pixels) and save them sequentially with `Set and Name.`

### Step 3. Load DeepLabCut File
Choose a scored DeepLabCut file (this should work with either the CSV or .h5 file, but I usually use the .h5).
Expand All @@ -46,3 +45,32 @@ This will quickly generate you a CSV file with basic stats on your videos, such

### Optional. Save ROIs for Future Analysis
The `Save ROIs to file` allows you to save your defined ROIs to a CSV file, which can be loaded later to allow for consistency and replication of your analysis.

## Example Files
For users who do not have DeepLabCut installed, example `.h5` and `.csv` ROI files are provided in the `h5data/` and `shapes/` directories respectively. These can be used to test the program's functionality.

## Batch Processing
This tool now supports processing multiple DeepLabCut files with corresponding ROI files in a batch.

**Note on Performance:** Batch processing can be a time-consuming operation, especially when dealing with a large number of files, long videos, or high frame rates. The application will display a progress bar in the terminal to indicate its progress. Please be patient, as it may take a significant amount of time to complete.

1. **Prepare your batch CSV file:** Create a CSV file with two columns. The first column should contain the full paths to your `shape.csv` files (the ROI definitions), and the second column should contain the full paths to your DeepLabCut `.h5` or `.csv` files. Do not forget the header row !
Example `example_batchinput.csv`:

| shape_file_path | file_path |
|------------------------|------------------------|
| shapes/circ_right_1.csv | h5data/random_mouse.h5 |
2. **Click "Process Batch":** In the GUI, click the "Process Batch" button.
3. **Select the batch CSV file:** A file dialog will open. Select the batch CSV file you prepared in step 1.
4. **View Results:** The tool will process each file pair listed in the batch CSV. An `output.csv` file will be generated in the current working directory (`/DLC_ROI_tool`). This file will contain the analysis results for each processed DeepLabCut file.

The `output.csv` file will have the following format:
- First column: Name of the DeepLabCut file (`.h5` or `.csv`).
- Subsequent columns: Time spent (in seconds) and number of entries for each defined ROI. Column names will follow the pattern `[ROI Name] time spent` and `[ROI Name] entries`.

Example `output.csv`:

| h5_file | ROI1 time spent | ROI2 time spent | ROI1 entries | ROI2 entries |
|------------|-----------------|-----------------|--------------|--------------|
| video1.h5 | 15.3 | 20.1 | 5 | 3 |
| video2.csv | 10.5 | 25.9 | 8 | 2 |
1 change: 1 addition & 0 deletions ROI_ENV.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ dependencies:
- prompt-toolkit==3.0.5
- pygments==2.6.1
- tables==3.6.1
- tqdm==4.67.1
- traitlets==4.3.3
- wcwidth==0.2.2
364 changes: 273 additions & 91 deletions ROI_tool.py

Large diffs are not rendered by default.

183 changes: 148 additions & 35 deletions SelectionObject.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


import tkinter as tk
from tkinter import *
from PIL import Image, ImageTk
Expand All @@ -12,50 +10,93 @@
import random
import glob
class SelectionObject:
""" Widget to display a rectangular area on given canvas defined by two points
representing its diagonal.
""" Widget to display a rectangular or circular area on given canvas defined by two points
representing its diagonal (for rectangle) or defining the bounding box (for circle).
"""
def __init__(self, canvas, select_opts):
def __init__(self, canvas, select_opts, shape_type_var, dim1_entry, dim2_entry):
# Create a selection objects for updating.
self.canvas = canvas
self.select_opts1 = select_opts
self.width = self.canvas.cget('width')
self.height = self.canvas.cget('height')
self.width = int(self.canvas.cget('width'))
self.height = int(self.canvas.cget('height'))
self.shape_type_var = shape_type_var
self.current_shape = None
self.dimension_text = None
self.dim1_entry = dim1_entry
self.dim2_entry = dim2_entry

# Store current shape coordinates for resizing
self.current_x1 = None
self.current_y1 = None
self.current_x2 = None
self.current_y2 = None


# Options for areas outside rectanglar selection.
# Options for areas outside selection.
select_opts1 = self.select_opts1.copy()
select_opts1.update({'state': tk.HIDDEN}) # Hide initially.
# Separate options for area inside rectanglar selection.
# Separate options for area inside selection.
select_opts2 = dict(dash=(2, 2), fill='', outline='white', state=tk.HIDDEN)

# Initial extrema of inner and outer rectangles.
imin_x, imin_y, imax_x, imax_y = 0, 0, 1, 1
omin_x, omin_y, omax_x, omax_y = 0, 0, self.width, self.height

self.rects = (
# Area *outside* selection (inner) rectangle.
self.canvas.create_rectangle(omin_x, omin_y, omax_x, imin_y, **select_opts1),
self.canvas.create_rectangle(omin_x, imin_y, imin_x, imax_y, **select_opts1),
self.canvas.create_rectangle(imax_x, imin_y, omax_x, imax_y, **select_opts1),
self.canvas.create_rectangle(omin_x, imax_y, omax_x, omax_y, **select_opts1),
# Inner rectangle.
self.canvas.create_rectangle(imin_x, imin_y, imax_x, imax_y, **select_opts2)
self.outer_rects = (
self.canvas.create_rectangle(0, 0, self.width, 1, **select_opts1),
self.canvas.create_rectangle(0, 0, 1, self.height, **select_opts1),
self.canvas.create_rectangle(self.width-1, 0, self.width, self.height, **select_opts1),
self.canvas.create_rectangle(0, self.height-1, self.width, self.height, **select_opts1)
)
self.inner_shape = None # Initialize inner_shape to None

def update(self, start, end, shape_type):
self.hide() # Hide previous shape

# Delete previous shape if it exists
if self.inner_shape:
self.canvas.delete(self.inner_shape)
self.inner_shape = None

# Options for the inner shape - include fill and stipple
select_opts2 = dict(dash=(2, 2), fill='red', outline='white', stipple='gray25')

if shape_type == "Rectangle":
imin_x, imin_y, imax_x, imax_y = self._get_coords(start, end)
self.inner_shape = self.canvas.create_rectangle(imin_x, imin_y, imax_x, imax_y, **select_opts2)

# Store the coordinates
self.current_x1 = imin_x
self.current_y1 = imin_y
self.current_x2 = imax_x
self.current_y2 = imax_y

# Hide outer rectangles for rectangle as fill is now on inner shape
for rect in self.outer_rects:
self.canvas.itemconfigure(rect, state=tk.HIDDEN)

self._display_dimensions(imin_x, imin_y, imax_x, imax_y, shape_type)

elif shape_type == "Circle":
center_x = (start[0] + end[0]) / 2
center_y = (start[1] + end[1]) / 2
radius = max(abs(start[0] - end[0]), abs(start[1] - end[1])) / 2
x1 = center_x - radius
y1 = center_y - radius
x2 = center_x + radius
y2 = center_y + radius
self.inner_shape = self.canvas.create_oval(x1, y1, x2, y2, **select_opts2)

# Store the coordinates (bounding box)
self.current_x1 = x1
self.current_y1 = y1
self.current_x2 = x2
self.current_y2 = y2

def update(self, start, end):
# Current extrema of inner and outer rectangles.
imin_x, imin_y, imax_x, imax_y = self._get_coords(start, end)
omin_x, omin_y, omax_x, omax_y = 0, 0, self.width, self.height
# Hide outer rectangles for circle (already done, but keep for clarity)
for rect in self.outer_rects:
self.canvas.itemconfigure(rect, state=tk.HIDDEN)

# Update coords of all rectangles based on these extrema.
self.canvas.coords(self.rects[0], omin_x, omin_y, omax_x, imin_y),
self.canvas.coords(self.rects[1], omin_x, imin_y, imin_x, imax_y),
self.canvas.coords(self.rects[2], imax_x, imin_y, omax_x, imax_y),
self.canvas.coords(self.rects[3], omin_x, imax_y, omax_x, omax_y),
self.canvas.coords(self.rects[4], imin_x, imin_y, imax_x, imax_y),
self._display_dimensions(x1, y1, x2, y2, shape_type)

self.canvas.update_idletasks() # Force canvas update

for rect in self.rects: # Make sure all are now visible.
self.canvas.itemconfigure(rect, state=tk.NORMAL)

def _get_coords(self, start, end):
""" Determine coords of a polygon defined by the start and
Expand All @@ -64,6 +105,78 @@ def _get_coords(self, start, end):
return (min((start[0], end[0])), min((start[1], end[1])),
max((start[0], end[0])), max((start[1], end[1])))

def _display_dimensions(self, x1, y1, x2, y2, shape_type):
self.dim1_entry.delete(0, tk.END)
self.dim2_entry.delete(0, tk.END)
if shape_type == "Rectangle":
width = abs(x2 - x1)
height = abs(y2 - y1)
self.dim1_entry.insert(0, f"{width:.2f}")
self.dim2_entry.insert(0, f"{height:.2f}")
elif shape_type == "Circle":
diameter = max(abs(x2 - x1), abs(y2 - y1))
self.dim1_entry.insert(0, f"{diameter:.2f}")
self.dim2_entry.insert(0, f"{diameter:.2f}") # Set height to diameter for circle


def hide(self):
for rect in self.rects:
self.canvas.itemconfigure(rect, state=tk.NORMAL)
if self.inner_shape: # Check if inner_shape exists before hiding
self.canvas.itemconfigure(self.inner_shape, state=tk.HIDDEN)
for rect in self.outer_rects:
self.canvas.itemconfigure(rect, state=tk.HIDDEN)

def reset_shape(self):
self.hide()
if self.inner_shape: # Delete shape on reset
self.canvas.delete(self.inner_shape)
self.inner_shape = None
self.dim1_entry.delete(0, tk.END)
self.dim2_entry.delete(0, tk.END)
# Reset stored coordinates on shape reset
self.current_x1 = None
self.current_y1 = None
self.current_x2 = None
self.current_y2 = None


def update_from_dimensions(self, dim1, dim2, shape_type):
# Delete previous shape if it exists
if self.inner_shape:
self.canvas.delete(self.inner_shape)
self.inner_shape = None

# Options for the inner shape - include fill and stipple
select_opts2 = dict(dash=(2, 2), fill='red', outline='white', stipple='gray25')

# Use stored top-left coordinates as the anchor point
new_x1 = self.current_x1 if self.current_x1 is not None else 0
new_y1 = self.current_y1 if self.current_y1 is not None else 0

if shape_type == "Rectangle":
width = dim1
height = dim2
new_x2 = new_x1 + width
new_y2 = new_y1 + height
self.inner_shape = self.canvas.create_rectangle(new_x1, new_y1, new_x2, new_y2, **select_opts2)

# Hide outer rectangles for rectangle as fill is now on inner shape
for rect in self.outer_rects:
self.canvas.itemconfigure(rect, state=tk.HIDDEN)

elif shape_type == "Circle":
diameter = dim1
# For circle, dim2 should be equal to dim1 (diameter)
if dim2 is None or dim2 == "": # If dim2 is not provided or empty, use dim1
dim2 = dim1
new_x2 = new_x1 + diameter
new_y2 = new_y1 + diameter
self.inner_shape = self.canvas.create_oval(new_x1, new_y1, new_x2, new_y2, **select_opts2)

# Update stored coordinates after resizing
self.current_x1 = new_x1
self.current_y1 = new_y1
self.current_x2 = new_x2
self.current_y2 = new_y2

self._display_dimensions(new_x1, new_y1, new_x2, new_y2, shape_type)
self.canvas.update_idletasks() # Force canvas update
2 changes: 2 additions & 0 deletions example_batchinput.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
shape_file_path,h5_file_path
shapes/circ_right_1.csv,h5data/random_mouse.h5
Binary file added h5data/random_mouse.h5
Binary file not shown.
2 changes: 2 additions & 0 deletions shapes/circ_right_1.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ROI,TLX,TLY,BRX,BRY,FPS,ShapeType
Circ_right_1,460.125,66.28571428571428,614.25,222.85714285714283,30.0,Circle
Binary file removed static/ROI.jpg
Binary file not shown.
Binary file added static/ROI_tool.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed static/jump.PNG
Binary file not shown.