Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f0fe0f518 | ||
|
|
31b9e85912 | ||
|
|
a220035126 | ||
|
|
a10fdbee50 | ||
|
|
236dd82871 | ||
|
|
f5b8e24e01 | ||
|
|
84b9c2674c | ||
|
|
bcadb5b9f7 | ||
|
|
4171dd2bc4 | ||
|
|
afbe73cd21 | ||
|
|
1246c2870a |
No files matched your search
@@ -161,3 +161,7 @@ env
|
||||
*.ipynb
|
||||
.direnv
|
||||
result
|
||||
dodgeballtracker/runs
|
||||
data
|
||||
outputs
|
||||
runs
|
||||
@@ -0,0 +1,2 @@
|
||||
good-images-for-training
|
||||
label-studio-exports
|
||||
@@ -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)
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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_ = 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),
|
||||
})
|
||||
TrackedDetectedObject = tensor_of(Float64,
|
||||
list(DetectedObject.columns) + ["object_id", "index"])
|
||||
|
||||
|
||||
Color = tuple[int, int, int]
|
||||
|
||||
Color = tuple[int, int, int]
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
ps.matplotlib
|
||||
ps.torchWithCuda
|
||||
ps.jupyter
|
||||
ps.pytest
|
||||
pythonPackages.opencv4
|
||||
pythonPackages.boxmot
|
||||
pythonPackages.ultralytics
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)));
|
||||
@@ -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);
|
||||
Whitespace-only changes.
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
*.json
|
||||
*.mp4
|
||||
@@ -0,0 +1,4 @@
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = [
|
||||
"."
|
||||
]
|
||||
+163
-122
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in new issue
Block a user