Skip to content

Commit

Permalink
roi metric sizes
Browse files Browse the repository at this point in the history
  • Loading branch information
sronilsson committed Sep 17, 2024
1 parent 76d1c33 commit c5a3f2e
Show file tree
Hide file tree
Showing 24 changed files with 727 additions and 286 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
# Setup configuration
setuptools.setup(
name="Simba-UW-tf-dev",
version="2.1.4",
version="2.1.6",
author="Simon Nilsson, Jia Jie Choong, Sophia Hwang",
author_email="[email protected]",
description="Toolkit for computer classification and analysis of behaviors in experimental animals",
Expand Down
1 change: 0 additions & 1 deletion simba/SimBA.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,6 @@ def train_multiple_models_from_meta(self, config_path=None):
def importBoris(self):
ann_folder = askdirectory()
boris_appender = BorisAppender(config_path=self.config_path, data_dir=ann_folder)
boris_appender.create_boris_master_file()
threading.Thread(target=boris_appender.run).start()

def importSolomon(self):
Expand Down
7 changes: 7 additions & 0 deletions simba/assets/lookups/yolo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
path: C:\troubleshooting\coco_data # dataset root dir
train: ../images/501_MA142_Gi_CNO_0514 # train images (relative to 'path') 128 images
val: ../images/F0_gq_CNO_0621 # val images (relative to 'path') 128 images
test: ../images/FL_gq_CNO_0625_78

names:
0: animal_1
Empty file.
180 changes: 180 additions & 0 deletions simba/bounding_box_tools/yolo/geometries_to_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import os
from typing import Optional, Union, Dict, Tuple
from pycocotools import mask

import cv2
import json
from simba.utils.read_write import read_df, get_video_meta_data, read_frm_of_video
import numpy as np
from simba.mixins.geometry_mixin import GeometryMixin
from shapely.geometry import Polygon
from datetime import datetime
from simba.utils.checks import check_int, check_instance, check_valid_array
from simba.utils.enums import Formats
from skimage.draw import polygon

def geometry_to_rle(geometry: Union[np.ndarray, Polygon], img_size: Tuple[int, int]):
"""
Converts a geometry (polygon or NumPy array) into a Run-Length Encoding (RLE) mask, suitable for object detection or segmentation tasks.
:param geometry: The geometry to be converted into an RLE. It can be either a shapely Polygon or a (n, 2) np.ndarray with vertices.
:param img_size: A tuple `(height, width)` representing the size of the image in which the geometry is to be encoded. This defines the dimensions of the output binary mask.
:return:
"""
check_instance(source=geometry_to_rle.__name__, instance=geometry, accepted_types=(Polygon, np.ndarray))
if isinstance(geometry, (Polygon,)):
geometry = geometry.exterior.coords
else:
check_valid_array(data=geometry, source=geometry_to_rle.__name__, accepted_ndims=(2,), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
binary_mask = np.zeros(img_size, dtype=np.uint8)
rr, cc = polygon(geometry[:, 0].flatten(), geometry[:, 1].flatten(), img_size)
binary_mask[rr, cc] = 1
rle = mask.encode(np.asfortranarray(binary_mask))
rle['counts'] = rle['counts'].decode('utf-8')
return rle

def geometries_to_coco(geometries: Dict[str, np.ndarray],
video_path: Union[str, os.PathLike],
save_dir: Union[str, os.PathLike],
version: Optional[int] = 1,
description: Optional[str] = None,
licences: Optional[str] = None):
"""
:example:
>>> data_path = r"C:\troubleshooting\mitra\project_folder\csv\outlier_corrected_movement_location\FRR_gq_Saline_0624.csv"
>>> animal_data = read_df(file_path=data_path, file_type='csv', usecols=['Nose_x', 'Nose_y', 'Tail_base_x', 'Tail_base_y', 'Left_side_x', 'Left_side_y', 'Right_side_x', 'Right_side_y']).values.reshape(-1, 4, 2)[0:20].astype(np.int32)
>>> animal_polygons = GeometryMixin().bodyparts_to_polygon(data=animal_data)
>>> animal_polygons = GeometryMixin().multiframe_minimum_rotated_rectangle(shapes=animal_polygons)
>>> animal_polygons = GeometryMixin().geometries_to_exterior_keypoints(geometries=animal_polygons)
>>> animal_polygons = GeometryMixin.keypoints_to_axis_aligned_bounding_box(keypoints=animal_polygons)
>>> animal_polygons = {0: animal_polygons}
>>> geometries_to_coco(geometries=animal_polygons, video_path=r'C:\troubleshooting\mitra\project_folder\videos\FRR_gq_Saline_0624.mp4', save_dir=r"C:\troubleshooting\coco_data")
"""

categories = []
for cnt, i in enumerate(geometries.keys()): categories.append({'id': i, 'name': i, 'supercategory': i})
results = {'info': {'year': datetime.now().year, 'version': version, 'description': description}, 'licences': licences, 'categories': categories}
video_data = get_video_meta_data(video_path)
w, h = video_data['width'], video_data['height']
images = []
annotations = []
img_names = []
if not os.path.isdir(save_dir): os.makedirs(save_dir)
save_img_dir = os.path.join(save_dir, 'img')
if not os.path.isdir(save_img_dir): os.makedirs(save_img_dir)
for category_cnt, (category_id, category_data) in enumerate(geometries.items()):
for img_cnt in range(category_data.shape[0]):
img_geometry = category_data[img_cnt]
img_name = f'{video_data["video_name"]}_{img_cnt}.png'
if img_name not in img_names:
images.append({'id': img_cnt, 'width': w, 'height': h, 'file_name': img_name})
img = read_frm_of_video(video_path=video_path, frame_index=img_cnt)
img_save_path = os.path.join(save_img_dir, img_name)
cv2.imwrite(img_save_path, img)
img_names.append(img_name)
annotation_id = category_cnt * img_cnt + 1
d = GeometryMixin().get_shape_lengths_widths(shapes=Polygon(img_geometry))
a_h, a_w, a_a = d['max_length'], d['max_width'], d['max_area']
bbox = [int(category_data[img_cnt][0][0]), int(category_data[img_cnt][0][1]), int(a_w), int(a_h)]
rle = geometry_to_rle(geometry=img_geometry, img_size=(h, w))
annotation = {'id': annotation_id, 'image_id': img_cnt, 'category_id': category_id, 'bbox': bbox, 'area': a_a, 'iscrowd': 0, 'segmentation': rle}
annotations.append(annotation)
results['images'] = images
results['annotations'] = annotations
with open(os.path.join(save_dir, f"annotations.json"), "w") as final:
json.dump(results, final)


def geometries_to_yolo(geometries: Dict[Union[str, int], np.ndarray],
video_path: Union[str, os.PathLike],
save_dir: Union[str, os.PathLike],
verbose: Optional[bool] = True,
sample: Optional[int] = None,
obb: Optional[bool] = False) -> None:
"""
Converts geometrical shapes (like polygons) into YOLO format annotations and saves them along with corresponding video frames as images.
:param Dict[Union[str, int], np.ndarray geometries: A dictionary where the keys represent category IDs (either string or int), and the values are NumPy arrays of shape `(n_frames, n_points, 2)`. Each entry in the array represents the geometry of an object in a particular frame (e.g., keypoints or polygons).
:param Union[str, os.PathLike] video_path: Path to the video file from which frames are extracted. The video is used to extract images corresponding to the geometrical annotations.
:param Union[str, os.PathLike] save_dir: The directory where the output images and YOLO annotation files will be saved. Images will be stored in a subfolder `images/` and annotations in `labels/`.
:param verbose: If `True`, prints progress while processing each frame. This can be useful for monitoring long-running tasks. Default is `True`.
:param sample: If provided, only a random sample of the geometries will be used for annotation. This value represents the number of frames to sample. If `None`, all frames will be processed. Default is `None`.
:param obb: If `True`, uses oriented bounding boxes (OBB) by extracting the four corner points of the geometries. Otherwise, axis-aligned bounding boxes (AABB) are used. Default is `False`.
:return None:
:example:
>>> data_path = r"C:\troubleshooting\mitra\project_folder\csv\outlier_corrected_movement_location\501_MA142_Gi_CNO_0514.csv"
>>> animal_data = read_df(file_path=data_path, file_type='csv', usecols=['Nose_x', 'Nose_y', 'Tail_base_x', 'Tail_base_y', 'Left_side_x', 'Left_side_y', 'Right_side_x', 'Right_side_y']).values.reshape(-1, 4, 2).astype(np.int32)
>>> animal_polygons = GeometryMixin().bodyparts_to_polygon(data=animal_data)
>>> poygons = GeometryMixin().multiframe_minimum_rotated_rectangle(shapes=animal_polygons)
>>> animal_polygons = GeometryMixin().geometries_to_exterior_keypoints(geometries=poygons)
>>> animal_polygons = {0: animal_polygons}
>>> geometries_to_yolo(geometries=animal_polygons, video_path=r'C:\troubleshooting\mitra\project_folder\videos\501_MA142_Gi_CNO_0514.mp4', save_dir=r"C:\troubleshooting\coco_data", sample=500, obb=True)
"""

video_data = get_video_meta_data(video_path)
categories = list(geometries.keys())
w, h = video_data['width'], video_data['height']
if not os.path.isdir(save_dir): os.makedirs(save_dir)
save_img_dir = os.path.join(save_dir, 'images')
save_labels_dir = os.path.join(save_dir, 'labels')
if not os.path.isdir(save_img_dir): os.makedirs(save_img_dir)
if not os.path.isdir(save_labels_dir): os.makedirs(save_labels_dir)
results, samples = {}, None
if sample is not None:
check_int(name='sample', value=sample, min_value=1, max_value=geometries[categories[0]].shape[0])
samples = np.random.choice(np.arange(0, geometries[categories[0]].shape[0]-1), sample)
for category_cnt, (category_id, category_data) in enumerate(geometries.items()):
for img_cnt in range(category_data.shape[0]):
if sample is not None and img_cnt not in samples:
continue
else:
if verbose:
print(f'Writing category {category_cnt}, Image: {img_cnt}.')
img_geometry = category_data[img_cnt]
img_name = f'{video_data["video_name"]}_{img_cnt}.png'
if not obb:
shape_stats = GeometryMixin.get_shape_statistics(shapes=Polygon(img_geometry))
x_center = shape_stats['centers'][0][0] / w
y_center = shape_stats['centers'][0][1] / h
width = shape_stats['widths'][0] / w
height = shape_stats['lengths'][0] / h
img_results = ' '.join([str(category_id), str(x_center), str(y_center), str(width), str(height)])
else:
img_geometry = img_geometry[1:]
x1, y1 = img_geometry[0][0] / w, img_geometry[0][1] / h
x2, y2 = img_geometry[1][0] / w, img_geometry[1][1] / h
x3, y3 = img_geometry[2][0] / w, img_geometry[2][1] / h
x4, y4 = img_geometry[3][0] / w, img_geometry[3][1] / h
img_results = ' '.join([str(category_id), str(x1), str(y1), str(x2), str(y2), str(x3), str(y3), str(x4), str(y4)])
if img_name not in results.keys():
img = read_frm_of_video(video_path=video_path, frame_index=img_cnt)
img_save_path = os.path.join(save_img_dir, img_name)
cv2.imwrite(img_save_path, img)
results[img_name] = [img_results]
else:
results[img_name].append(img_results)

for k, v in results.items():
name = k.split(sep='.', maxsplit=2)[0]
file_name = os.path.join(save_labels_dir, f'{name}.txt')
with open(file_name, mode='wt', encoding='utf-8') as myfile:
myfile.write('\n'.join(v))



#def geometries_to_yolo_obb(geometries: Dict[Union[str, int], np.ndarray]):



#
#
#
data_path = r"C:\troubleshooting\mitra\project_folder\csv\outlier_corrected_movement_location\FL_gq_CNO_0625.csv"
animal_data = read_df(file_path=data_path, file_type='csv', usecols=['Nose_x', 'Nose_y', 'Tail_base_x', 'Tail_base_y', 'Left_side_x', 'Left_side_y', 'Right_side_x', 'Right_side_y']).values.reshape(-1, 4, 2).astype(np.int32)
animal_polygons = GeometryMixin().bodyparts_to_polygon(data=animal_data)
poygons = GeometryMixin().multiframe_minimum_rotated_rectangle(shapes=animal_polygons)
animal_polygons = GeometryMixin().geometries_to_exterior_keypoints(geometries=poygons)
# animal_polygons = GeometryMixin.keypoints_to_axis_aligned_bounding_box(keypoints=animal_polygons)
animal_polygons = {0: animal_polygons}
geometries_to_yolo(geometries=animal_polygons, video_path=r'C:\troubleshooting\mitra\project_folder\videos\FL_gq_CNO_0625.mp4', save_dir=r"C:\troubleshooting\coco_data", sample=500, obb=True)
84 changes: 84 additions & 0 deletions simba/bounding_box_tools/yolo/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import os
import numpy as np
import torch
from ultralytics import YOLO
from typing import Union, Optional, Tuple
import multiprocessing
import pandas as pd
import functools
from simba.utils.enums import Defaults
from simba.utils.read_write import get_video_meta_data, read_img_batch_from_video_gpu, find_core_cnt

def fit_yolo(initial_weights: Union[str, os.PathLike],
data: Union[str, os.PathLike],
project_path: Union[str, os.PathLike],
epochs: Optional[int] = 5,
batch: Optional[Union[int, float]] = 16):
"""
:param initial_weights:
:param data:
:param project_path:
:param epochs:
:param batch:
:return:
:example:
>>> fit_yolo(initial_weights=r"C:\troubleshooting\coco_data\weights\yolov8n-obb.pt", data=r"C:\troubleshooting\coco_data\model.yaml", project_path=r"C:\troubleshooting\coco_data\mdl", batch=16)
"""

if not torch.cuda.is_available():
raise ModuleNotFoundError('No GPU detected.')
model = YOLO(initial_weights)
model.train(data=data, epochs=epochs, project=project_path, batch=batch)

def inference_yolo(weights: Union[str, os.PathLike],
video_path: Union[str, os.PathLike],
batch: Optional[Union[int, float]] = 100,
verbose: Optional[bool] = False,
save_dir: Optional[Union[str, os.PathLike]] = None):

torch.set_num_threads(8)
model = YOLO(weights, verbose=verbose)
# model.export(format='engine')
# model.to('cuda')
results = []
out_cols = ['FRAME', 'CLASS', 'CONFIDENCE', 'X1', 'Y1', 'X2', 'Y2', 'X3', 'Y3', 'X4', 'Y4']
if os.path.isfile(video_path):
_ = get_video_meta_data(video_path=video_path)
video_results = model(video_path)
for frm_cnt, frm in enumerate(video_results):
if frm.obb is not None:
data = np.array(frm.obb.data.cpu()).astype(np.float32)
else:
data = np.array(frm.boxes.data.cpu()).astype(np.float32)
classes = np.unique(data[:, -1])
for c in classes:
cls_data = data[np.argwhere(data[:, -1] == c)].reshape(-1, data.shape[1])
cls_data = cls_data[np.argmax(data[:, -2].flatten())]
cord_data = np.array([cls_data[0], cls_data[1], cls_data[0], cls_data[3], cls_data[1], cls_data[3], cls_data[2], cls_data[1]]).astype(np.int32)
results.append([frm_cnt, cls_data[-1], cls_data[-2]] + list(cord_data))
results = pd.DataFrame(results, columns=out_cols)

if not save_dir:
return results


# r = inference_yolo(weights=r"C:\troubleshooting\coco_data\mdl\train\weights\best.pt",
# video_path=r"C:\troubleshooting\mitra\project_folder\videos\clipped\501_MA142_Gi_CNO_0514_clipped.mp4",
# batch=100)


#fit_yolo(initial_weights=r"C:\troubleshooting\coco_data\weights\yolov8n-obb.pt", data=r"C:\troubleshooting\coco_data\model.yaml", project_path=r"C:\troubleshooting\coco_data\mdl", batch=16)


# """
# initial_weights=r"C:\Users\sroni\Downloads\yolov8n.pt", data=r"C:\troubleshooting\coco_data\model.yaml", epochs=30, project_path=r"C:\troubleshooting\coco_data\mdl", batch=16)
# """
#
#
#



60 changes: 58 additions & 2 deletions simba/data_processors/cuda/circular_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ def sliding_bearing(x: np.ndarray,
.. csv-table::
:header: EXPECTED RUNTIMES
:file: ../../../docs/tables/sliding_bearing.csv
:widths: 10, 90
:widths: 10, 45, 45
:align: center
:header-rows: 1
Expand Down Expand Up @@ -638,7 +638,7 @@ def sliding_angular_diff(x: np.ndarray,
.. csv-table::
:header: EXPECTED RUNTIMES
:file: ../../../docs/tables/sliding_angular_diff.csv
:widths: 10, 90
:widths: 10, 45, 45
:align: center
:header-rows: 1
Expand Down Expand Up @@ -674,4 +674,60 @@ def sliding_angular_diff(x: np.ndarray,
results = results.copy_to_host().astype(np.int32)
return results

@cuda.jit()
def _rotational_direction(data, stride, results):
r = cuda.grid(1)
l = int(r - stride[0])
if (r < 0) or (r > data.shape[0] - 1):
return
elif (l < 0):
return
else:
l_val, r_val = data[l], data[r]
angle_diff = r_val - l_val
if angle_diff > np.pi:
angle_diff -= 2 * np.pi
elif angle_diff < -np.pi:
angle_diff += 2 * np.pi
if angle_diff == 0:
results[r] = 0
elif angle_diff > 0:
results[r] = 1
else:
results[r] = 2

def rotational_direction(data: np.ndarray, stride: Optional[int] = 1) -> np.ndarray:
"""
Computes the rotational direction between consecutive data points in a circular space, where the angles wrap
around at 360 degrees. The function uses GPU acceleration via CUDA to process the data in parallel.
The result array contains values:
* `0` where there is no change between points.
* `1` where the angle has increased in the positive direction.
* `2` where the angle has decreased in the negative direction.
.. seealso::
:func:`simba.mixins.circular_statistics.CircularStatisticsMixin.rotational_direction` for jitted CPU method.
:param np.ndarray data: 1D array of angular data (in degrees) to analyze. The data will be internally converted to radians and wrapped between [0, 360) degrees before processing.
:param Optional[int] stride: The stride or gap between data points for which the rotational direction is calculated. Default is 1.
:return: A 1D array of integers of the same length as `data`, where each element indicates the rotational direction between the current and previous point based on the stride. The first `stride` elements in the result will be initialized to -1 since they cannot be compared.
:rtype: np.ndarray
:example:
>>> data = np.random.randint(0, 365, (100))
>>> p = rotational_direction(data=data)
"""
data = np.deg2rad(data % 360)
results = np.full((data.shape[0]), fill_value=-1, dtype=np.int16)
results_dev = cuda.to_device(results)
stride = np.array([stride])
stride_dev = cuda.to_device(stride)
data_dev = cuda.to_device(data)
bpg = (data.shape[0] + (THREADS_PER_BLOCK - 1)) // THREADS_PER_BLOCK
_rotational_direction[bpg, THREADS_PER_BLOCK](data_dev, stride_dev, results_dev)
return results_dev.copy_to_host()



5 changes: 4 additions & 1 deletion simba/data_processors/cuda/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def find_midpoints(x: np.ndarray,
.. csv-table::
:header: EXPECTED RUNTIMES
:file: ../../../docs/tables/find_midpoints.csv
:widths: 10, 90
:widths: 10, 45, 45
:align: center
:class: simba-table
:header-rows: 1
Expand Down Expand Up @@ -390,3 +390,6 @@ def find_midpoints(x: np.ndarray,






Loading

0 comments on commit c5a3f2e

Please sign in to comment.