Compare commits

..
11 Commits
Author SHA1 Message Date
didier 0f0fe0f518 with a coffee 2024-11-23 09:16:24 -05:00
didier 31b9e85912 with a glass of water 2024-11-16 20:35:27 -05:00
didier a220035126 fixed tests. 2024-11-16 18:27:34 -05:00
didier a10fdbee50 Going somewhere with a coffee 2024-11-16 09:16:25 -05:00
didier 236dd82871 Going somewhere before going to the gym 2024-11-15 13:33:14 -05:00
didier f5b8e24e01 Going somewhere with a coffee. 2024-11-15 10:00:22 -05:00
didier 84b9c2674c Going somewhere without a glass of wine. 2024-11-13 11:28:30 -05:00
didier bcadb5b9f7 Going somewhere with a glass of wine. 2024-11-08 19:46:49 -05:00
didier 4171dd2bc4 cleaned up code. 2024-11-08 11:57:32 -05:00
didier afbe73cd21 Deleted frontend 2024-11-05 08:41:01 -05:00
didier 1246c2870a Merge branch 'new-ui' into 'main'
New ui

See merge request all-dressed-programming/dodgeball-tracker!1
2024-11-04 23:51:09 +00:00
25 changed files with 797 additions and 820 deletions

No files matched your search

+4
View File
@@ -161,3 +161,7 @@ env
*.ipynb
.direnv
result
dodgeballtracker/runs
data
outputs
runs
+2
View File
@@ -0,0 +1,2 @@
good-images-for-training
label-studio-exports
+76 -25
View File
@@ -1,22 +1,24 @@
from contextlib import closing
import json5 as json
from contextlib import closing, contextmanager
from pathlib import Path
import sqlite3
from dodgeballtracker.metadata import VideoMetadata
from dodgeballtracker.helper import time_tracker
from dodgeballtracker.logic import (DodgeballTracker,
get_court_perspective_transform)
from dodgeballtracker.logic import DodgeballTracker
from dodgeballtracker.data_dumper import SQLiteDataDumper, DataDumper
import numpy as np
import logging
import click
from typing import ContextManager
import hashlib
from typing import ContextManager, Iterator
from dodgeballtracker.video_writer import (MP4VVideoWriter,
VideoWriter,
DetectedObjectBox,
MP4VVideoWriterWithFrame)
here = Path(__file__).parent
input_dir = here / '..' / 'data'
output_dir = here / '..' / 'outputs'
@click.group()
@@ -25,10 +27,13 @@ def cli(debug):
logging.basicConfig(filename='tracker.log', level=logging.DEBUG)
def get_output(input_: Path, proposed_output: Path | None) -> Path:
def get_output_dir(input_: Path, proposed_output: Path | None) -> Path:
if proposed_output is not None:
proposed_output.mkdir(parents=True, exist_ok=True)
return proposed_output
return output_dir / input_.name
output = output_dir / input_.name
output.mkdir(parents=True, exist_ok=True)
return output
def get_video_writer(display_frame: bool,
@@ -41,30 +46,76 @@ def get_video_writer(display_frame: bool,
return closing(MP4VVideoWriter.from_frame(video_input, video_output, thresholds))
@cli.command()
@click.option('--input', '-i', 'input_',
type=click.Path(readable=True, path_type=Path))
@click.option('--output', '-o', type=click.Path(path_type=Path))
@click.option('--display-video', '-d', type=bool, is_flag=True, show_default=True, default=False)
def process(input_: Path, output: Path | None, display_video: bool):
output = get_output(input_, output)
def get_file_sha256(file_: Path) -> str:
with open(file_, 'rb', buffering=0) as f:
return hashlib.file_digest(f, 'sha256').hexdigest()
@contextmanager
def create_data_dumper(input_file: Path,
output_file: Path,
metadata: VideoMetadata) -> Iterator[DataDumper]:
with closing(sqlite3.connect(output_file)) as conn:
conn.execute('PRAGMA foreign_keys = 1')
dumper = SQLiteDataDumper(conn)
dumper.start(input_file, get_file_sha256(input_file), metadata)
yield dumper
def deserialized_video_metadata(positional_data: Path | None) -> VideoMetadata | None:
if not positional_data:
return None
with open(positional_data, 'rt') as file_:
data = json.load(file_)
return VideoMetadata(**data)
def process_logic(input_: Path,
output_dir: Path | None,
metadata_file: Path | None,
display_video: bool):
metadata = deserialized_video_metadata(metadata_file)
output_dir = get_output_dir(input_, output_dir)
detected_box_objects = [
DetectedObjectBox.from_viridis(threshold) for threshold in
np.arange(0.5, 1, 0.05)
]
transformation_matrix = get_court_perspective_transform()
with get_video_writer(display_video, input_, output_dir / 'video.mov') as video_writer:
with create_data_dumper(input_, output_dir / 'data.sqlite', metadata) as data_dumper:
tracker = DodgeballTracker(input_,
video_writer,
data_dumper,
detected_box_objects,
metadata.get_view_perspective_matrix())
with get_video_writer(display_video, input_, output) as video_writer:
tracker = DodgeballTracker(input_,
output,
video_writer,
detected_box_objects,
transformation_matrix)
with time_tracker():
tracker.main()
with time_tracker():
tracker.main()
@cli.command()
def train():
pass
@cli.command()
@click.option('--input', '-i', 'input_',
type=click.Path(readable=True, path_type=Path))
@click.option('--output-dir', '-o', type=click.Path(path_type=Path))
@click.option('--metadata-file', '-m', type=click.Path(path_type=Path, file_okay=True, exists=True))
@click.option('--display-video', '-d', type=bool, is_flag=True, show_default=True, default=False)
def process(input_: Path,
output_dir: Path | None,
metadata_file: Path | None,
display_video: bool):
process_logic(input_,
output_dir,
metadata_file,
display_video)
if __name__ == "__main__":
cli()
# process_logic(Path('/home/didier/all-dressed-programming/dodgeball-tracker/data/experiments/rue-honore-beaugrand-ouest-001.MOV'),
# Path('/home/didier/all-dressed-programming/dodgeball-tracker/outputs/benchmark-001'),
# False)
+128
View File
@@ -0,0 +1,128 @@
import abc
import pandas as pd
from pathlib import Path
from dodgeballtracker.metadata import VideoMetadata
from dodgeballtracker.typing import TrackedDetectedObject, VideoTimestamp
from sqlite3 import Connection
import pandera as pa
class DataDumper(abc.ABC):
@abc.abstractmethod
def write_boxes(self,
tracked_obj: pd.DataFrame,
ts: VideoTimestamp,
frame_index: int) -> None:
pass
@abc.abstractmethod
def start(self, video_path: Path, hexa_hash: str) -> None:
pass
class SQLiteDataDumper(DataDumper):
def __init__(self, conn: Connection):
self._conn = conn
self._video_id = None
def _create_tables(self):
cur = self._conn.cursor()
cur.executescript('''
CREATE TABLE IF NOT EXISTS video (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_name TEXT NOT NULL,
file_sha256_hex TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 1,
UNIQUE(file_sha256_hex)
);
''')
cur.executescript('''
CREATE TABLE IF NOT EXISTS video_frame(
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp_ DOUBLE PRECISION NOT NULL,
index_ INTEGER NOT NULL,
video_id INTEGER NOT NULL,
nbr_detected_object INTEGER NOT NULL,
FOREIGN KEY(video_id) REFERENCES video(id),
UNIQUE(video_id, index_)
);
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS tracked_detected_object(
id INTEGER PRIMARY KEY AUTOINCREMENT,
xmin DOUBLE PRECISION NOT NULL,
ymin DOUBLE PRECISION NOT NULL,
xmax DOUBLE PRECISION NOT NULL,
ymax DOUBLE PRECISION NOT NULL,
bird_view_x DOUBLE PRECISION,
bird_view_y DOUBLE PRECISION,
score DOUBLE PRECISION NOT NULL,
category INTEGER NOT NULL,
object_id INTEGER NOT NULL,
video_frame_id INTEGER NOT NULL,
FOREIGN KEY(video_frame_id) REFERENCES video_frame(id)
);
''')
self._conn.commit()
def start(self,
video_path: Path,
hexa_hash: str,
metadata: VideoMetadata | None = None) -> None:
self._create_tables()
cur = self._conn.cursor()
res = cur.execute("""
INSERT INTO video(file_name, file_sha256_hex)
VALUES (?, ?)
ON CONFLICT(file_sha256_hex) DO UPDATE SET count = count + 1
RETURNING id
""", (video_path.name, hexa_hash))
self._video_id = res.fetchone()[0]
self._conn.commit()
def _is_empty_df(self, df):
return not df.shape[0]
@pa.check_io(df=TrackedDetectedObject)
def write_boxes(self,
df: pd.DataFrame,
ts: VideoTimestamp,
frame_index: int) -> None:
if self._is_empty_df(df):
return
cur = self._conn.cursor()
res = cur.execute('''
INSERT INTO video_frame(timestamp_, index_, video_id, nbr_detected_object )
VALUES (?, ?, ?, 1)
ON CONFLICT(video_id, index_)
DO UPDATE SET nbr_detected_object = nbr_detected_object + 1
RETURNING id
''', (ts, frame_index, self._video_id))
frame_id = res.fetchone()[0]
cols = ['xmin', 'ymin', 'xmax', 'ymax', 'score', 'category',
'object_id']
cur.executemany('''
INSERT INTO tracked_detected_object(xmin,
ymin,
xmax,
ymax,
score,
category,
object_id,
video_frame_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (list(x[cols]) + [frame_id] for x in df.iloc))
self._conn.commit()
+41 -81
View File
@@ -1,4 +1,3 @@
import json
from pathlib import Path
import logging
import torch
@@ -6,11 +5,12 @@ from ultralytics import YOLO
import cv2
from dodgeballtracker.helper import iter_frames
from dodgeballtracker.video_writer import VideoWriter, DetectedObjectBox
from dodgeballtracker.data_dumper import DataDumper
import numpy as np
import pandas as pd
import pandera as pa
from boxmot import DeepOcSort as DeepOCSORT
from .typing import VideoFrame, DetectedObject_
from .typing import VideoFrame, DetectedObject, TrackedDetectedObject
logger = logging.getLogger(__name__)
@@ -28,31 +28,21 @@ def get_best_available_torch_device() -> torch.device:
return torch.device('cpu')
def get_court_perspective_transform():
"""
"""
empirical_top_left = np.float32([416, 175])
empirical_top_right = np.float32([905, 173])
empirical_bottom_left = np.float32([51, 702])
empirical_bottom_right = np.float32([1188, 695])
court_top_left = np.float32([0, 0])
court_top_right = np.float32([9, 0])
court_bottom_left = np.float32([0, 18])
court_bottom_right = np.float32([9, 18])
empirical_source = np.float32([empirical_top_left,
empirical_top_right,
empirical_bottom_left,
empirical_bottom_right])
pixel_per_meter = 50
empirical_target = np.float32([court_top_left,
court_top_right,
court_bottom_left,
court_bottom_right]) * pixel_per_meter
return cv2.getPerspectiveTransform(empirical_source, empirical_target)
class BirdViewCreator:
def __init__(self, view_perspective_matrix: np.matrix):
self._view_perspective_matrix = view_perspective_matrix
@pa.check_io(df=TrackedDetectedObject, out=TrackedDetectedObject)
def append_bird_view(self, df: pd.DataFrame) -> pd.DataFrame:
v = np.matrix([
pd.Series(dtype='float32', data=np.ones(df.shape[0])),
df['xmin'],
df['xmax']
])
ret = self._view_perspective_matrix @ v
ret_df = pd.DataFrame(ret).rename(columns={0: 'physical_x', 1: 'physical_y'})
return df.join(ret_df)
@@ -60,63 +50,25 @@ class DodgeballTracker:
def __init__(self,
input_file: Path,
output_file: Path,
video_writer: VideoWriter,
data_dumper: DataDumper,
detected_box_objects: list[DetectedObjectBox],
transformation_matrix):
bird_view_creator: BirdViewCreator):
self.input_path = input_file
self.output_path = output_file
self.output_json_path = output_file.with_suffix(".json")
self.video_writer = video_writer
self._data_dumper = data_dumper
self.detected_box_objects = sorted(detected_box_objects, reverse=True)
self.transformation_matrix = transformation_matrix
self.points = []
self._bird_view_creator = bird_view_creator
def write_frame(self, writer, frame) -> None:
cv2.imshow("Frame", frame)
writer.write(frame)
def extract_box(self, data) -> tuple[int, int, int, int]:
return int(data[0]), int(data[1]), int(data[2]), int(data[3])
def print_box(self, data, frame_idx, frame_ts):
xmin, ymin, xmax, ymax = self.extract_box(data)
xavg = (xmax + xmin) / 2
v = np.array([xavg, ymax, 1])
ret = self.transformation_matrix @ v
x, y = ret[0] / ret[2], ret[1] / ret[2]
object_id = data[-1]
self.points.append({"x": x,
"y": y,
"frameIndex": frame_idx,
"frameTimestamp": frame_ts,
"objectId": int(object_id)
})
def print_tracker_data(self, tracker_data, frame_idx, frame_ts):
for d in tracker_data:
self.print_box(d, frame_idx, frame_ts)
def plot_box_to_frame(self, data, frame: VideoFrame):
confidence = float(data[4])
for detected_box_object in self.detected_box_objects:
if detected_box_object.min_threshold < confidence:
# xmin, ymin, xmax, ymax = self.extract_box(data)
# cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), detected_box_object.color, 2)
return True
return False
@pa.check_output(DetectedObject_)
@pa.check_output(DetectedObject)
def run_model_on_frame(self, model, frame: VideoFrame):
model_outputs = model(frame)[0]
boxes = model_outputs.boxes.data.cpu().numpy()
columns=['xmin', 'ymin', 'xmax', 'ymin', 'score', 'category']
return pd.DataFrame(boxes, columns=columns)
columns=['xmin', 'ymin', 'xmax', 'ymax', 'score', 'category']
ret = pd.DataFrame(boxes, columns=columns)
return ret
def persist_points(self):
with open(self.output_json_path, "wt") as file_:
json.dump({"points": self.points}, file_, indent=4)
def _get_tracker(self) -> DeepOCSORT:
return DeepOCSORT(
@@ -129,13 +81,17 @@ class DodgeballTracker:
"""
load the pre-trained YOLOv8n model
"""
return YOLO("yolo11n.pt", verbose=True)
def update_tracker(self, tracker: DeepOCSORT, detected_objs, frame: VideoFrame):
data_ = tracker.update(detected_objs.to_numpy(), frame)
columns = ['xmin', 'ymin', 'xmax', 'ymin', 'object_id', 'score', 'category', 'index']
return pd.DataFrame(data_, columns=columns)
my_model = Path(
'/home/didier/all-dressed-programming/dodgeball-tracker/runs/detect/train31/weights/best.pt')
return YOLO(my_model, verbose=False)
@pa.check_io(detected_objs=DetectedObject, out=TrackedDetectedObject)
def update_tracker(self, tracker: DeepOCSORT, detected_objs: pd.DataFrame, frame: VideoFrame):
data = tracker.update(detected_objs.to_numpy(), frame)
columns = ['xmin', 'ymin', 'xmax', 'ymax', 'object_id', 'score', 'category', 'index']
if not data.size:
return pd.DataFrame({col: pd.Series(dtype='float64') for col in columns})
return pd.DataFrame(data, columns=columns)
def main(self):
# define some constants
@@ -147,12 +103,16 @@ class DodgeballTracker:
# run the YOLO model on the frame
detected_objs = self.run_model_on_frame(model, frame)
tracked_obj = self.update_tracker(tracker, detected_objs, frame)
# self.video_writer.plot_boxes_to_frame(detected_objs, frame)
#
# self.video_writer.write_frame(frame)
self._data_dumper.write_boxes(
self._bird_view_creator.append_bird_view(tracked_obj),
ts,
i)
self.video_writer.plot_boxes_to_frame(tracked_obj, frame)
self.video_writer.write_frame(frame)
if self.video_writer.wait():
break
video_cap.release()
self.persist_points()
+42
View File
@@ -0,0 +1,42 @@
from pydantic import BaseModel
import numpy as np
import cv2
class KnownImagePoint(BaseModel):
width: float
height: float
@classmethod
def from_vec(cls, vec: tuple[float, float]) -> 'KnownImagePoint':
return cls(width=vec[0], height=vec[1])
class BirdViewPoint(BaseModel):
x: float
y: float
@classmethod
def from_vec(cls, vec: tuple[float, float]) -> 'BirdViewPoint':
return cls(x=vec[0], y=vec[1])
class KnownPoint(BaseModel):
image: KnownImagePoint
bird_view: BirdViewPoint
class VideoMetadata(BaseModel):
knownPoints: list[KnownPoint]
def get_view_perspective_matrix(self) -> np.ndarray | None:
if len(self.knownPoints) != 4:
return None
empirical_source = np.float32(
[[k.image.width, k.image.height] for k in self.knownPoints])
empirical_target = np.float32(
[[k.bird_view.x, k.bird_view.y] for k in self.knownPoints])
pixel_per_meter = 100
return cv2.getPerspectiveTransform(empirical_source,
empirical_target * pixel_per_meter)
+35
View File
@@ -0,0 +1,35 @@
from ultralytics import YOLO
from pathlib import Path
def foo():
model = YOLO("yolo11x.pt")
example = Path(
'/home/didier/all-dressed-programming/dodgeball-tracker/data/good-images-for-training/PB120196.JPG')
results = model.predict(example)
print('print done predicting')
print(f'done predicting; {len(results)} results')
for res in results:
print(f" >>> {res.boxes.data}")
def main():
# Load a model
model = YOLO("yolo11s.pt") # load a pretrained model (recommended for training)
# Train the model
train = '/home/didier/all-dressed-programming/dodgeball-tracker/data/wheels_004.yaml'
model.train(data=Path(train),
epochs=150,
imgsz=640,
cache='ram')
print('Done training')
bad_example = Path("/home/didier/all-dressed-programming/dodgeball-tracker/data/label-studio-exports/2024-11-12_001/images/train/2a6267f7-PB120166.JPG")
example = Path('/home/didier/all-dressed-programming/dodgeball-tracker/data/good-images-for-training/PB120196.JPG')
results = model.predict(bad_example)
print(f'done predicting; {len(results)} results')
for res in results:
print(f" >>> {res.boxes.data}")
if __name__ == '__main__':
foo()
+7 -46
View File
@@ -1,59 +1,20 @@
from typing import NewType
from dataclasses import dataclass
import numpy as np
import pandera as pa
from pandera.dtypes import Float32
XCoord = NewType('XCoord', float)
YCoord = NewType('YCoord', float)
Score = NewType('Confidence', float)
Category = NewType('Category', float)
@dataclass
class Point:
x: XCoord
y: YCoord
from pandera.dtypes import Float32, Float64
VideoFrame = NewType('Frame', float)
VideoTimestamp = float
@dataclass
class DetectedObject:
xmin: XCoord
ymin: YCoord
xmax: XCoord
ymax: YCoord
score: Score
category: Category
def as_int_box(self) -> tuple[tuple[int, int], tuple[int, int]]:
return (int(self.xmin), int(self.ymin)), (int(self.xmax), int(self.ymax))
def as_nparray(self):
return np.array([self.xmin, self.ymax, self.xmax, self.ymax, self.score, self.category])
def tensor_of(data_type: pa.DataType, cols: list[str]) -> pa.DataFrameSchema:
return pa.DataFrameSchema({col: pa.Column(data_type) for col in cols})
DetectedObject_ = pa.DataFrameSchema({
"xmin": pa.Column(Float32),
"ymin": pa.Column(Float32),
"xmax": pa.Column(Float32),
"ymin": pa.Column(Float32),
"score": pa.Column(Float32),
"category": pa.Column(Float32),
})
DetectedObject = tensor_of(Float32, ["xmin", "ymin", "xmax", "ymax", "score", "category"])
TrackedDetectedObject = tensor_of(Float64,
list(DetectedObject.columns) + ["object_id", "index"])
TrackedDetectedObject_ = pa.DataFrameSchema({
"xmin": pa.Column(Float32),
"ymin": pa.Column(Float32),
"xmax": pa.Column(Float32),
"ymin": pa.Column(Float32),
"object_id": pa.Column(Float32),
"score": pa.Column(Float32),
"category": pa.Column(Float32),
"index": pa.Column(Float32),
})
Color = tuple[int, int, int]
+34 -9
View File
@@ -1,6 +1,10 @@
import abc
from pathlib import Path
from dodgeballtracker.typing import VideoFrame, DetectedObject, Color
import pandas as pd
import pandera as pa
from dodgeballtracker.typing import (VideoFrame, Color, TrackedDetectedObject)
import matplotlib
import cv2
@@ -44,7 +48,7 @@ class VideoWriter(abc.ABC):
pass
@abc.abstractmethod
def plot_boxes_to_frame(self, data: list[DetectedObject], frame: VideoFrame) -> None:
def plot_boxes_to_frame(self, data: pd.DataFrame, frame: VideoFrame) -> None:
pass
@abc.abstractmethod
@@ -58,7 +62,7 @@ class VideoWriter(abc.ABC):
class NoopVideoWriter(VideoWriter):
def plot_boxes_to_frame(self, boxes: list[DetectedObject], frame: VideoFrame) -> None:
def plot_boxes_to_frame(self, boxes: pd.DataFrame, frame: VideoFrame) -> None:
pass
def write_frame(self, frame: VideoFrame) -> None:
@@ -80,16 +84,37 @@ class MP4VVideoWriter(VideoWriter):
def write_frame(self, frame: VideoFrame) -> None:
self._video_writer.write(frame)
def plot_box_to_frame(self, data: DetectedObject, frame: VideoFrame) -> None:
confidence = float(data.score)
def write_box_id(self, frame: VideoFrame, box: pd.Series):
bottom_left_corner_of_text = (int(box['xmin']), int(box['ymax']))
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.5
font_color = (255, 255, 255)
thickness = 1
line_type = 2
id_ = str(int(box['object_id']))
cv2.putText(frame,
id_,
bottom_left_corner_of_text,
font,
font_scale,
font_color,
thickness,
line_type)
def plot_box_to_frame(self, data: pd.Series, frame: VideoFrame) -> None:
confidence = data['score']
for threshold in self._thresholds:
if threshold.min_threshold < confidence:
# xmin, ymin, xmax, ymax = self._extract_box(data)
cv2.rectangle(frame, *data.as_int_box(), threshold.color, 2)
start_point = data[['xmin', 'ymin']].array.astype('int32')
end_point = data[['xmax', 'ymax']].array.astype('int32')
cv2.rectangle(frame, start_point, end_point, threshold.color, 2)
self.write_box_id(frame, data)
return
def plot_boxes_to_frame(self, boxes: list[DetectedObject], frame: VideoFrame) -> None:
for box in boxes:
@pa.decorators.check_io(boxes=TrackedDetectedObject)
def plot_boxes_to_frame(self, boxes: pd.DataFrame, frame: VideoFrame) -> None:
boxes_that_passe_threshold = boxes[boxes['score'] > 0.5]
for i, box in boxes_that_passe_threshold.iterrows():
self.plot_box_to_frame(box, frame)
+1
View File
@@ -62,6 +62,7 @@
ps.matplotlib
ps.torchWithCuda
ps.jupyter
ps.pytest
pythonPackages.opencv4
pythonPackages.boxmot
pythonPackages.ultralytics
-72
View File
@@ -1,72 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Animation</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="" />
<link rel="icon" href="favicon.png">
<link rel="stylesheet" href="style.css">
<script src="animation.js" type="module"></script>
</head>
<body>
<h1>Hello, world!</h1>
<svg width="900" height="450" >
<rect x="0" y="0" width="900" height="450" class="court-border"></rect>
<line x1="450" y1="0" x2="450" y2="450" class="court-border"></line>
<line x1="300" y1="0" x2="300" y2="450" class="return-line"></line>
<line x1="600" y1="0" x2="600" y2="450" class="return-line"></line>
<rect x="0" y="157.5" width="15" height="15"
style="fill: red;" id="rect-1"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: blue;" id="rect-2"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: yellow;" id="rect-3"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: pink;" id="rect-4"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: purple;" id="rect-5"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: black;" id="rect-6"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: red; fill-opacity: 0;" id="rect-7"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: blue; fill-opacity: 0;" id="rect-8"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: yellow; fill-opacity: 0;" id="rect-9"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: pink; fill-opacity: 0;" id="rect-10"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: purple; fill-opacity: 0;" id="rect-11"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: black; fill-opacity: 0;" id="rect-12"></rect>
</svg>
<div>
<button id="play">play</button> <br>
<button id="pause">pause</button>
<br>
<input type="number" id="seek-value">
<button id="seek">seek</button>
</div>
</body>
</html>
-66
View File
@@ -1,66 +0,0 @@
import anime from "https://esm.sh/animejs@3.2.2";
const second = 1000;
const canvasWidth = Number.parseFloat(document.querySelector("svg").getAttribute("width"));
const canvasHeight = Number.parseFloat(document.querySelector("svg").getAttribute("height"));
function gaussianRandom(mean=0, stdev=1) {
const u = 1 - Math.random(); // Converting [0,1) to (0,1]
const v = Math.random();
const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
// Transform to the desired mean and standard deviation:
return z * stdev + mean;
}
function* brownian(n){
let x = 0;
for(let i = 0; i < n; i ++){
x += gaussianRandom(0, 10);
console.log(x);
yield x;
}
}
function* pseudoBrownian(n, limit){
let x = 0;
for(let i = 0; i < n; i ++){
let delta = gaussianRandom(0, 10);
if(Math.abs(x + delta) > limit){
delta = -1 * delta
}
x += delta;
yield x;
}
}
function* keyFrames(duration, framePerSeconds){
const deltaX = canvasWidth / (duration * framePerSeconds);
const limit = 0.75 * (canvasHeight / 2);
for(const deltaY of pseudoBrownian(duration * framePerSeconds, limit)){
yield {translateX: `+=${deltaX}`, translateY: deltaY};
}
}
const duration = 60;
const timeline = anime.timeline({
duration: duration * second,
autoplay: false,
easing: 'linear'});
for(let i = 1; i <= 12; i++){
timeline.add({
targets: `#rect-${i}`,
keyframes: Array.from(keyFrames(duration, 30)),
}, 0)
}
document.querySelector('#play').onclick = timeline.play;
document.querySelector('#pause').onclick = timeline.pause;
document.querySelector('#seek').onclick = function(){
const seekVal = Number.parseFloat(second * document.querySelector('#seek-value').value);
timeline.pause();
timeline.seek(seekVal);
}
-90
View File
@@ -1,90 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Dodgeball Tracker</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="" />
<link rel="icon" href="favicon.png">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="style.css">
<script src="main-old.js" type="module"></script>
</head>
<body>
<h1 class="title is-1">Dodgeball Tracker</h1>
<div id="new"></div>
<h3 id="first-ts"></h3>
<div>
<select>
<option value="lmbn_n_cuhk03_d">lmbn_n_cuhk03_d</option>
</select>
</div>
<svg width="900" height="450" id="first" style="background-color: lightgray">
</svg>
<br>
<svg width="900" height="450" >
<rect x="0" y="0" width="900" height="450" class="court-border"></rect>
<line x1="450" y1="0" x2="450" y2="450" class="court-border"></line>
<line x1="300" y1="0" x2="300" y2="450" class="return-line"></line>
<line x1="600" y1="0" x2="600" y2="450" class="return-line"></line>
<rect x="0" y="157.5" width="15" height="15"
style="fill: red;" id="rect-1"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: blue;" id="rect-2"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: yellow;" id="rect-3"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: pink;" id="rect-4"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: purple;" id="rect-5"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="fill: black;" id="rect-6"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: red; fill-opacity: 0;" id="rect-7"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: blue; fill-opacity: 0;" id="rect-8"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: yellow; fill-opacity: 0;" id="rect-9"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: pink; fill-opacity: 0;" id="rect-10"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: purple; fill-opacity: 0;" id="rect-11"></rect>
<rect x="0" y="157.5" width="15" height="15"
style="stroke: black; fill-opacity: 0;" id="rect-12"></rect>
</svg>
<div>
<button id="play">play</button> <br>
<button id="pause">pause</button>
<br>
<input type="number" id="seek-value">
<button id="seek">seek</button>
</div>
</body>
</html>
-18
View File
@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Dodgeball Tracker</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="" />
<link rel="icon" href="favicon.png">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="style.css">
<script src="main.js" type="module"></script>
</head>
<body>
</body>
</html>
-118
View File
@@ -1,118 +0,0 @@
import anime from "https://esm.sh/animejs@3.2.2";
import m from "https://esm.sh/mithril@2.2.2"
import colormap from "https://esm.sh/colormap@2.3.2"
const second = 1000;
const canvasWidth = Number.parseFloat(document.querySelector("svg").getAttribute("width"));
const canvasHeight = Number.parseFloat(document.querySelector("svg").getAttribute("height"));
function gaussianRandom(mean=0, stdev=1) {
const u = 1 - Math.random(); // Converting [0,1) to (0,1]
const v = Math.random();
const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
// Transform to the desired mean and standard deviation:
return z * stdev + mean;
}
function* brownian(n){
let x = 0;
for(let i = 0; i < n; i ++){
x += gaussianRandom(0, 10);
console.log(x);
yield x;
}
}
function* pseudoBrownian(n, limit){
let x = 0;
for(let i = 0; i < n; i ++){
let delta = gaussianRandom(0, 10);
if(Math.abs(x + delta) > limit){
delta = -1 * delta
}
x += delta;
yield x;
}
}
function* keyFrames(duration, framePerSeconds){
const deltaX = canvasWidth / (duration * framePerSeconds);
const limit = 0.75 * (canvasHeight / 2);
for(const deltaY of pseudoBrownian(duration * framePerSeconds, limit)){
yield {translateX: `+=${deltaX}`, translateY: deltaY};
}
}
const duration = 60;
const timeline = anime.timeline({
duration: duration * second,
autoplay: false,
easing: 'linear'});
for(let i = 1; i <= 12; i++){
timeline.add({
targets: `#rect-${i}`,
keyframes: Array.from(keyFrames(duration, 30)),
}, 0)
}
document.querySelector('#play').onclick = timeline.play;
document.querySelector('#pause').onclick = timeline.pause;
document.querySelector('#seek').onclick = function(){
const seekVal = Number.parseFloat(second * document.querySelector('#seek-value').value);
timeline.pause();
timeline.seek(seekVal);
}
function generatePoints(points, frames){
const lst = [];
const colors = colormap({
colormap: 'viridis',
nshades: new Set(points.map(p => p.objectId)).size,
format: 'hex',
alpha: 1
});
for(const point of points){
if(frames.indexOf(point.frameIndex) !== -1){
lst.push(m("text", {x: point.y, y: point.x, fill: colors[point.objectId]}, point.objectId));
}
}
return m("g", lst);
}
function getTimeStamp(points, frameId){
for(const point of points){
if(point.frameIndex === frameId){
return `${point.frameTimestamp / 1000} seconds`;
}
}
}
const url = "/dodgeball-tracker/outputs/output-lmbn_n_cuhk03_d.json";
const resp = await fetch(url);
const points = await resp.json();
function range(min, max){
const ret = [];
while(min < max){
ret.push(min);
min ++;
}
return ret
}
function getCourtLines(){
return [
m("line", {x1: 450, x2: 450, y1: 0, y2: 450, class: "court-border"}),
m("line", {x1: 300, x2: 300, y1: 0, y2: 450, class: "return-line"}),
m("line", {x1: 600, x2: 600, y1: 0, y2: 450, class: "return-line"})
]
}
m.render(document.getElementById("first"), generatePoints(points.points, range(-10, 400)));
m.render(document.getElementById("first-ts"), m("span", getTimeStamp(points.points, 80)));
-143
View File
@@ -1,143 +0,0 @@
import m from "https://esm.sh/mithril@2.2.2"
import colormap from "https://esm.sh/colormap@2.3.2"
import {range} from "./util.js";
function gaussianRandom(mean=0, stdev=1) {
const u = 1 - Math.random(); // Converting [0,1) to (0,1]
const v = Math.random();
const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
// Transform to the desired mean and standard deviation:
return z * stdev + mean;
}
function* brownian(n){
let x = 0;
for(let i = 0; i < n; i ++){
x += gaussianRandom(0, 10);
console.log(x);
yield x;
}
}
function* pseudoBrownian(n, limit){
let x = 0;
for(let i = 0; i < n; i ++){
let delta = gaussianRandom(0, 10);
if(Math.abs(x + delta) > limit){
delta = -1 * delta
}
x += delta;
yield x;
}
}
function* keyFrames(duration, framePerSeconds){
const deltaX = canvasWidth / (duration * framePerSeconds);
const limit = 0.75 * (canvasHeight / 2);
for(const deltaY of pseudoBrownian(duration * framePerSeconds, limit)){
yield {translateX: `+=${deltaX}`, translateY: deltaY};
}
}
function generatePoints(points, frames){
const lst = [];
const colors = colormap({
colormap: 'viridis',
nshades: new Set(points.map(p => p.objectId)).size,
format: 'hex',
alpha: 1
});
for(const point of points){
if(frames.indexOf(point.frameIndex) !== -1){
lst.push(m("text", {x: point.y, y: point.x, fill: colors[point.objectId]}, point.objectId));
}
}
return m("g", lst);
}
function getTimeStamp(points, frameId){
for(const point of points){
if(point.frameIndex === frameId){
return `${point.frameTimestamp / 1000} seconds`;
}
}
}
// const url = "/dodgeball-tracker/outputs/output-lmbn_n_cuhk03_d.json";
// const resp = await fetch(url);
// const points = await resp.json();
function getCourtLines(){
return [
m("line", {x1: 450, x2: 450, y1: 0, y2: 450, class: "court-border"}),
m("line", {x1: 300, x2: 300, y1: 0, y2: 450, class: "return-line"}),
m("line", {x1: 600, x2: 600, y1: 0, y2: 450, class: "return-line"})
]
}
class WorkingTabs{
#datasets = []
async oninit(){
const url = "/dodgeball-tracker/outputs/index.json"
this.#datasets = (await m.request({method: "GET", url: url}))?.index ?? [];
}
tabs(){
return m("div.tabs",
m("ul",
m("li.is-active", m("a", "Info")),
m("li", m("a", "Video Player"))));
}
info(){
const options = this.#datasets.map(ds => m('option', {value: ds.id}, ds.label));
return m("div.field",
m("label.label", "Dataset"),
m("div.control",
m("div.select",
m("select", options))));
}
view(){
return m("div",
this.tabs(),
this.info())
}
}
const Court = {
view: function(){
const margin = 50
return m("svg.court-side", { width: 900 + 2 * margin, height: 450 + 2 * margin}, [
m("g.court-border#court", {transform: `translate(${margin} ${margin})`}, [
m("rect", {width: 900, height: 450, style: 'background-color: pink;'}),
getCourtLines()
])
])
}
}
// m.render(document.getElementById("new"), drawCourt());
// m.render(document.getElementById("first"), generatePoints(points.points, range(-10, 400)));
// m.render(document.getElementById("first-ts"), m("span", getTimeStamp(points.points, 80)));
const Main = {
view: function(){
return m("div", [
m("h1.title.is-1", "DodgeBall Tracker"),
m("div.columns.is-centered",
m("m.column.is-three-quarters",
m(Court),
m(WorkingTabs)))
])
}
}
m.mount(document.body, Main);
View File
Whitespace-only changes.
-16
View File
@@ -1,16 +0,0 @@
.court-border {
stroke: red;
stroke-width: 4px;
/*fill-opacity: 0;*/
fill: gray;
}
.court-side {
background-color: pink;
}
.return-line {
stroke: blue;
stroke-width: 2px;
fill-opacity: 0;
}
-12
View File
@@ -1,12 +0,0 @@
export function range(min, max=undefined){
if(max === undefined){
max = min;
min = 0;
}
const ret = [];
while(min < max){
ret.push(min);
min ++;
}
return ret
}
-2
View File
@@ -1,2 +0,0 @@
*.json
*.mp4
+4
View File
@@ -0,0 +1,4 @@
[tool.pytest.ini_options]
pythonpath = [
"."
]
+163 -122
View File
@@ -1,165 +1,206 @@
absl-py==2.1.0
anyio==4.2.0
appnope==0.1.3
aiodns==3.2.0
aiohappyeyeballs==2.3.6
aiohttp==3.10.5
aiosignal==1.3.1
annotated-types==0.7.0
anyio==4.4.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==2.4.1
astunparse==1.6.3
async-lru==2.0.4
async-timeout==4.0.3
async_generator==1.10
attrs==23.2.0
Babel==2.14.0
beautifulsoup4==4.12.2
Babel==2.15.0
bcrypt==4.1.3
beautifulsoup4==4.12.3
bleach==6.1.0
boxmot==10.0.51
build==1.0.3
boxmot==10.0.81
Brotli==1.1.0
brotlicffi==1.1.0.0
cachetools==5.3.2
certifi==2023.11.17
cffi==1.16.0
cfgv==3.4.0
certifi==2024.7.4
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.3.2
click==8.1.7
comm==0.2.1
contourpy==1.2.0
comm==0.2.2
contourpy==1.2.1
cryptography==43.0.0
curio==1.6
curl_cffi==0.7.1
cycler==0.12.1
Cython==3.0.8
debugpy==1.8.0
ddt==1.7.2
decorator==5.1.1
deep-sort-realtime==1.3.2
defusedxml==0.7.1
distlib==0.3.8
exceptiongroup==1.2.0
executing==2.0.1
defusedxml==0.8.0rc2
executing==2.1.0
fastjsonschema==2.19.1
filelock==3.13.1
filelock==3.15.1
filterpy==1.4.5
fonttools==4.47.2
fonttools==4.53.1
fqdn==1.5.1
fsspec==2023.12.2
ftfy==6.1.3
gdown==4.6.3
frozenlist==1.4.1
fsspec==2024.3.0
ftfy==6.2.3
future==1.0.0
gdown==5.2.0
gitdb==4.0.11
GitPython==3.1.41
google-auth==2.26.2
google-auth-oauthlib==1.2.0
grpcio==1.60.0
identify==2.5.33
idna==3.6
ipykernel==6.28.0
ipython==8.20.0
ipywidgets==8.1.1
GitPython==3.1.43
google-auth==2.30.0
google-auth-oauthlib==1.2.1
grpcio==1.64.1
gviz-api==1.10.0
h11==0.14.0
html5lib==1.1
httpcore==1.0.5
httpx==0.27.0
idna==3.7
iniconfig==2.0.0
ipykernel==6.29.5
ipython==8.27.0
ipywidgets==8.1.5
isoduration==20.11.0
jedi==0.19.1
Jinja2==3.1.3
joblib==1.3.2
jeepney==0.8.0
Jinja2==3.1.4
joblib==1.4.2
json5==0.9.14
jsonpointer==2.4
jsonschema==4.20.0
jsonpointer==3.0.0
jsonschema==4.22.0
jsonschema-specifications==2023.12.1
jupyter==1.0.0
jupyter==1.1.1
jupyter-console==6.6.3
jupyter-events==0.9.0
jupyter-lsp==2.2.1
jupyter_client==8.6.0
jupyter_core==5.7.1
jupyter_server==2.12.4
jupyter_server_terminals==0.5.1
jupyterlab==4.0.10
jupyterlab-widgets==3.0.9
jupyter-events==0.10.0
jupyter-lsp==2.2.5
jupyter_client==8.6.2
jupyter_core==5.7.2
jupyter_server==2.14.2
jupyter_server_terminals==0.5.3
jupyterlab==4.2.5
jupyterlab_pygments==0.3.0
jupyterlab_server==2.25.2
jupyterlab_server==2.27.3
jupyterlab_widgets==3.0.13
kiwisolver==1.4.5
lapx==0.5.5
lapx==0.5.11
loguru==0.7.2
Markdown==3.5.2
MarkupSafe==2.1.3
matplotlib==3.8.2
matplotlib-inline==0.1.6
lz4==4.3.3
Markdown==3.7
MarkupSafe==2.1.5
matplotlib==3.9.1
matplotlib-inline==0.1.7
mistune==3.0.2
mpmath==1.3.0
nbclient==0.9.0
nbconvert==7.14.1
nbformat==5.9.2
nest-asyncio==1.5.8
networkx==3.2.1
nodeenv==1.8.0
notebook==7.0.6
notebook_shim==0.2.3
numpy==1.24.4
multidict==6.1.0
multimethod==1.9.1
mutagen==1.47.0
mypy_extensions==1.0.0
nbclient==0.10.0
nbconvert==7.16.4
nbformat==5.10.4
nest_asyncio==1.6.0
networkx==3.3
notebook==7.2.2
notebook_shim==0.2.4
numpy==1.26.4
oauthlib==3.2.2
opencv-python==4.9.0.80
overrides==7.4.0
packaging==23.2
pandas==2.1.4
pandocfilters==1.5.0
parso==0.8.3
olefile==0.47
opencv @ file:///build/source/modules/python/package/dist/opencv-4.9.0-py3-none-any.whl#sha256=0b1c6093b66ec745df186877df1e6e72241dfe8dda392f023bc361fe1d5411ca
outcome==1.3.0.post0
overrides==7.7.0
packaging==24.1
pandas==2.2.2
pandera==0.20.4
pandocfilters==1.5.1
paramiko==3.5.0
parso==0.8.4
pexpect==4.9.0
pillow==10.2.0
pip-tools==7.3.0
platformdirs==4.1.0
pre-commit==3.6.0
prometheus-client==0.19.0
prompt-toolkit==3.0.43
protobuf==4.23.4
psutil==5.9.7
pillow==10.4.0
platformdirs==4.2.2
pluggy==1.5.0
prometheus_client==0.21.0
prompt_toolkit==3.0.47
protobuf==5.28.2
psutil==6.0.0
ptyprocess==0.7.0
pure-eval==0.2.2
pure_eval==0.2.2
py-cpuinfo==9.0.0
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycparser==2.21
Pygments==2.17.2
pyparsing==3.1.1
pyproject_hooks==1.0.0
pyasn1==0.6.0
pyasn1_modules==0.4.0
pybind11==2.13.5
pycares==4.4.0
pycparser==2.22
pycryptodomex==3.20.0
pydantic==2.8.2
pydantic_core==2.20.1
Pygments==2.18.0
pyparsing==3.1.2
PySocks==1.7.1
python-dateutil==2.8.2
pyspnego==0.10.2
pytest==8.3.2
python-dateutil==2.9.0.post0
python-json-logger==2.0.7
pytz==2023.3.post1
PyYAML==6.0.1
pyzmq==25.1.2
qtconsole==5.5.1
QtPy==2.4.1
referencing==0.32.1
regex==2023.12.25
requests==2.31.0
requests-oauthlib==1.3.1
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rpds-py==0.17.1
python-socks==2.4.4
pytz==2024.2
PyYAML==6.0.2
pyzmq==26.0.3
referencing==0.34.0
regex==2024.5.15
requests==2.32.3
requests-oauthlib==2.0.0
rfc3339_validator==0.1.4
rfc3986_validator==0.1.1
rpds-py==0.18.1
rsa==4.9
scikit-learn==1.4.0
scipy==1.11.4
seaborn==0.13.1
scikit-learn==1.5.0
scipy==1.14.0
seaborn==0.13.2
SecretStorage==3.3.3
Send2Trash==1.8.2
setuptools==72.1.0.post0
six==1.16.0
smmap==5.0.1
sniffio==1.3.0
smbprotocol==1.12.0
smmap==6.0.0
sniffio==1.3.1
sortedcontainers==2.4.0
soupsieve==2.5
stack-data==0.6.3
sympy==1.12
tensorboard==2.15.1
stack_data==0.6.3
sympy==1.12.1
tensorboard==2.17.0
tensorboard-data-server==0.7.2
terminado==0.18.0
thop==0.1.1.post2209072238
threadpoolctl==3.2.0
tinycss2==1.2.1
tomli==2.0.1
torch==2.1.2
torchvision==0.16.2
tornado==6.4
tqdm==4.66.1
traitlets==5.14.1
types-python-dateutil==2.8.19.20240106
typing_extensions==4.9.0
tzdata==2023.4
ultralytics==8.1.3
tensorboard-plugin-profile==2.11.1
tensorboard-plugin-wit==1.7.0
terminado==0.18.1
threadpoolctl==3.4.0
tinycss2==1.3.0
toml==0.10.2
torch==2.5.0
torchvision==0.20.0a0
tornado==6.4.1
tqdm==4.66.4
traitlets==5.14.3
trio==0.25.0
triton==3.1.0
typeguard==4.3.0
types-python-dateutil==2.9.0.20240316
typing_extensions==4.12.2
typing_inspect==0.9.0
tzdata==2024.1
ultralytics==8.3.13
ultralytics-thop==2.0.9
uri-template==1.3.0
urllib3==2.1.0
virtualenv==20.25.0
urllib3==2.2.2
wcwidth==0.2.13
webcolors==1.13
webencodings==0.5.1
websocket-client==1.7.0
Werkzeug==3.0.1
widgetsnbextension==4.0.9
websocket-client==1.8.0
websockets==12.0
Werkzeug==3.0.4
wheel==0.43.0
widgetsnbextension==4.0.13
wrapt==1.16.0
yacs==0.1.8
youtube-dl==2021.12.17
yarl==1.9.4
yt-dlp==2024.10.22
+138
View File
@@ -0,0 +1,138 @@
import sqlite3
import tempfile
from pathlib import Path
from sqlite3 import Connection
from dodgeballtracker.metadata import VideoMetadata
from dodgeballtracker.typing import TrackedDetectedObject
import pandas as pd
import pandera as pa
import numpy as np
import pytest
from contextlib import closing, contextmanager
from dodgeballtracker.data_dumper import SQLiteDataDumper
def list_tables(conn: Connection) -> set[str]:
cur = conn.cursor()
res = cur.execute('''
SELECT
name
FROM
sqlite_schema
WHERE
type ='table' AND
name NOT LIKE 'sqlite_%';
''')
return {row[0] for row in res}
def video_metadata() -> VideoMetadata:
data = {
}
return VideoMetadata()
@pytest.fixture
def tmp_dir():
with tempfile.TemporaryDirectory() as dir_:
yield Path(dir_)
def number_of_items_in_table(table: str, conn: Connection) -> int:
cur = conn.cursor()
res = cur.execute(f'SELECT COUNT(*) FROM {table}')
return res.fetchone()[0]
# @pytest.fixture
@contextmanager
def make_conn(tmp_dir: Path):
db_file= tmp_dir / 'db.sqlite'
conn_ = sqlite3.connect(db_file)
with closing(conn_) as open_conn:
open_conn.execute('PRAGMA foreign_keys = 1')
yield open_conn
@pytest.fixture
@pa.check_output(TrackedDetectedObject)
def mock_tracked_detected_object():
rd = np.random.RandomState(seed=109)
data = rd.rand(10, len(TrackedDetectedObject.columns))
return pd.DataFrame(data, columns=TrackedDetectedObject.columns)
def test_start(tmp_dir: Path):
with make_conn(tmp_dir) as conn:
dumper = SQLiteDataDumper(conn)
dumper.start(Path('foo/bar.MOV'), 'aaabbb')
assert dumper._video_id == 1
with make_conn(tmp_dir) as conn:
expt = {
'tracked_detected_object',
'video',
'video_frame'
}
assert expt == list_tables(conn)
def test_restart(tmp_dir: Path):
with make_conn(tmp_dir) as conn:
dumper = SQLiteDataDumper(conn)
dumper.start(Path('foo/bar.MOV'), 'aaabbb')
with make_conn(tmp_dir) as conn:
dumper = SQLiteDataDumper(conn)
dumper.start(Path('foo/bar.MOV'), 'aaabbb')
with make_conn(tmp_dir) as conn:
expt = {
'tracked_detected_object',
'video',
'video_frame'
}
assert expt == list_tables(conn)
def test_write_boxes_simple(tmp_dir: Path,
mock_tracked_detected_object: pd.DataFrame):
with make_conn(tmp_dir) as conn:
dumper = SQLiteDataDumper(conn)
dumper.start(Path('/foo/toto.MOV'), 'aabbcc')
dumper.write_boxes(mock_tracked_detected_object, 40018, 34)
with make_conn(tmp_dir) as conn:
assert 10 == number_of_items_in_table('tracked_detected_object', conn)
def test_write_boxes_multiple_times(tmp_dir: Path, mock_tracked_detected_object: pd.DataFrame):
with make_conn(tmp_dir) as conn:
dumper = SQLiteDataDumper(conn)
dumper.start(Path('/foo/toto.MOV'), 'aabbcc')
dumper.write_boxes(mock_tracked_detected_object, 40018, 34)
with make_conn(tmp_dir) as conn:
dumper = SQLiteDataDumper(conn)
dumper.start(Path('/foo/toto.MOV'), 'aabbcc')
dumper.write_boxes(mock_tracked_detected_object, 40018, 34)
with make_conn(tmp_dir) as conn:
assert 20 == number_of_items_in_table('tracked_detected_object', conn)
def test_write_boxes_with_empty_frame(tmp_dir: Path,
mock_tracked_detected_object: pd.DataFrame):
empty_tracked_detected_object = mock_tracked_detected_object[0:0]
with make_conn(tmp_dir) as conn:
dumper = SQLiteDataDumper(conn)
dumper.start(Path('/foo/toto.MOV'), 'aabbcc')
dumper.write_boxes(empty_tracked_detected_object, None,40018, 34)
with make_conn(tmp_dir) as conn:
assert 0 == number_of_items_in_table('tracked_detected_object', conn)
assert 0 == number_of_items_in_table('video_frame', conn)
+39
View File
@@ -0,0 +1,39 @@
import numpy as np
import pandas as pd
import pandera as pa
import pytest
from dodgeballtracker.logic import BirdViewCreator
from dodgeballtracker.typing import TrackedDetectedObject
@pytest.fixture
def view_perspective() -> np.matrix:
return np.matrix([
[200, 100, 0],
[100, 100, 0],
[0, 0, 1]
])
@pytest.fixture
@pa.check_io(out=TrackedDetectedObject)
def mock_data() -> pd.DataFrame:
return pd.DataFrame(
{
"xmin": pd.Series(dtype='float64', data=[10.0, 100.0]),
"ymin": pd.Series(dtype='float64', data=[30.0, 130.0]),
"xmax": pd.Series(dtype='float64', data=[55.0, 150.0]),
"ymax": pd.Series(dtype='float64', data=[65.0, 195.0]),
"score": pd.Series(dtype='float64', data=[0.66, 0.56]),
"category": pd.Series(dtype='float64', data=[3, 3]),
"object_id": pd.Series(dtype='float64', data=[73232, 837276]),
"index": pd.Series(dtype='float64', data=[0, 1]),
}
)
def test_get_bird_view(view_perspective: np.matrix,
mock_data: pd.DataFrame) -> None:
bvd = BirdViewCreator(view_perspective)
ret = bvd.append_bird_view(mock_data)
+83
View File
@@ -0,0 +1,83 @@
import pytest
import numpy as np
from dodgeballtracker.metadata import (VideoMetadata, KnownImagePoint,
BirdViewPoint, KnownPoint)
@pytest.fixture
def simple_transformation_matrix() -> np.matrix:
m = np.matrix([[2, 1], [1, 1]])
assert np.linalg.det(m) > 0.1
return m
def create_know_point(v, m) -> KnownPoint:
u = (m @ v).A[0]
return KnownPoint(image=KnownImagePoint.from_vec(v),
bird_view=BirdViewPoint.from_vec(u))
@pytest.fixture
def four_known_points(simple_transformation_matrix: np.matrix) -> list[
KnownPoint]:
return [
create_know_point([x, y], simple_transformation_matrix)
for x in [-100, 100]
for y in [-100, 100]
]
@pytest.fixture
def eight_known_points(simple_transformation_matrix: np.matrix,
four_known_points: list[KnownPoint]) -> list[KnownPoint]:
return [
create_know_point([x, y], simple_transformation_matrix)
for x in [-50, 50]
for y in [-50, 50]
] + four_known_points
@pytest.fixture
def simple_video_metadata(four_known_points) -> VideoMetadata:
return VideoMetadata(knownPoints=four_known_points)
@pytest.fixture
def mega_video_metadata(eight_known_points) -> VideoMetadata:
return VideoMetadata(knownPoints=eight_known_points)
@pytest.fixture
def broken_video_metadata(four_known_points) -> VideoMetadata:
return VideoMetadata(knownPoints=four_known_points[:2])
def test_get_view_perspective_matrix(simple_video_metadata: VideoMetadata) -> None:
m = simple_video_metadata.get_view_perspective_matrix()
expt = np.array([
[200, 100, 0],
[100, 100, 0],
[0, 0, 1]
])
np.testing.assert_array_almost_equal(m, expt)
@pytest.mark.xfail
def test_get_view_perspective_matrix_mega(mega_video_metadata: VideoMetadata) -> None:
"""
The issue with this test is that the transformation
matrix can only be computed is there's exactly 4 known
points. Obviously, in the real world we want more known
points and find a way to get a better estimate.
"""
m = mega_video_metadata.get_view_perspective_matrix()
expt = np.array([
[200, 100, 0],
[100, 100, 0],
[0, 0, 1]
])
np.testing.assert_array_almost_equal(m, expt)
def test_get_view_perspective_matrix_without_enough_data(broken_video_metadata: VideoMetadata) -> None:
m = broken_video_metadata.get_view_perspective_matrix()
assert m is None