Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add workflows for Windows and MacOS #93

Draft
wants to merge 89 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
8703ace
Test install
zhong-al Oct 11, 2024
26ecd62
Start tracks extractor test
zhong-al Oct 11, 2024
309c15d
Add data for tracks extractor test
zhong-al Oct 11, 2024
1ae921a
Check test on any push
zhong-al Oct 11, 2024
6da7f18
Try to fix torch missing
zhong-al Oct 11, 2024
9ac0c76
Try to fix torch missing
zhong-al Oct 11, 2024
c38275f
Fix hatchling missing
zhong-al Oct 12, 2024
6f66453
Add miniscene2behavior test
zhong-al Oct 12, 2024
af28c4b
Update tests
zhong-al Oct 14, 2024
f9dd66b
Start test for cvat2ultralytics and detector2cvat
zhong-al Oct 14, 2024
98b2b76
Fix cvat2ultralytics, move detection test files
zhong-al Oct 14, 2024
3407f48
Add miniscene2behavior test
zhong-al Oct 14, 2024
4ee974d
Ignore tool outputs
zhong-al Oct 14, 2024
15d1316
Adjust path
zhong-al Oct 14, 2024
6aa96f2
Update path
zhong-al Oct 14, 2024
d313209
Add player test and fix player globals
zhong-al Oct 14, 2024
a3347c5
Add cvat2slowfast test
zhong-al Oct 14, 2024
7076bb3
Comment out tests
zhong-al Oct 18, 2024
912d547
Add mocked tests
zhong-al Oct 18, 2024
06d01ee
Patch detector2cvat
zhong-al Oct 18, 2024
331ecc7
Patch player & add sample data
zhong-al Oct 18, 2024
1fe22af
Rename run functions
zhong-al Oct 18, 2024
869b17b
Test tracks extractor parser
zhong-al Oct 18, 2024
a25f893
Mock getTrackbarPos
zhong-al Oct 18, 2024
8b4c2c7
Test cvat2slowfast parser
zhong-al Oct 19, 2024
8308c60
Test cvat2ultralytics parsing
zhong-al Oct 19, 2024
c011e0e
Test detector2cvat parsing
zhong-al Oct 19, 2024
0348bd6
Test miniscene2behavior parsing
zhong-al Oct 19, 2024
ac12359
Test player parsing
zhong-al Oct 19, 2024
e761672
Update cvat2slowfast
zhong-al Oct 25, 2024
5a1478e
Update cvat2ultralytics
zhong-al Oct 25, 2024
656c40d
Update detector2cvat
zhong-al Oct 25, 2024
b55f1dc
Fix test
zhong-al Oct 25, 2024
520e1cc
Update miniscene2behavior
zhong-al Oct 25, 2024
316ab59
Update player
zhong-al Oct 25, 2024
9cfcfc0
Update tracks_extractor
zhong-al Oct 25, 2024
7606e6d
Extract model download + unzip
zhong-al Oct 25, 2024
b191425
Fix download
zhong-al Oct 25, 2024
7584e81
Download model at setup
zhong-al Oct 28, 2024
9ee7499
Add label2index json
zhong-al Oct 30, 2024
fdba887
Merge branch 'master' into enhancement/testing
zhong-al Nov 13, 2024
50feea9
Delete tracks_extractor output
zhong-al Nov 13, 2024
6a91fc2
Add clean up + runs
zhong-al Nov 13, 2024
da97183
Merge branch 'master' into enhancement/testing
zhong-al Nov 13, 2024
37dd750
Update arg parsing checks
zhong-al Nov 13, 2024
f7eb368
Remove slowfast model
zhong-al Nov 15, 2024
d69776b
Remove slowfast imports
zhong-al Nov 15, 2024
d883e40
Update requirements
zhong-al Nov 19, 2024
2b87a14
Add slowfast code
zhong-al Nov 19, 2024
1eed904
Just test 1 file on this branch
zhong-al Nov 19, 2024
d02c0a6
Correct requirements
zhong-al Nov 19, 2024
bab8b0c
Update model download and creation
zhong-al Nov 19, 2024
abf9369
Update dependencies
zhong-al Nov 19, 2024
bb855f2
Add slowfast license
zhong-al Nov 19, 2024
ce923b5
Update dependencies
zhong-al Nov 19, 2024
17f6a79
Update README
zhong-al Nov 19, 2024
a9f2deb
Fix TypeError: _BatchNorm.__init__()
zhong-al Nov 19, 2024
e4dee39
Add hub checkpoint check
zhong-al Nov 19, 2024
75bc26d
Fix format string
zhong-al Nov 19, 2024
e9f70e8
Check all loading methods
zhong-al Nov 21, 2024
71b6a5e
Check handling of invalid args/params
zhong-al Nov 21, 2024
a62d74a
Update path resolution
zhong-al Nov 21, 2024
9bb1726
Fix comparison
zhong-al Nov 21, 2024
6873627
Check test output
zhong-al Nov 21, 2024
8b40266
Correct reference annotation
zhong-al Nov 21, 2024
6510324
Fix bug: missing model.eval()
zhong-al Nov 21, 2024
7e20266
Remove debug print
zhong-al Nov 21, 2024
61bc4ad
Print diffs
zhong-al Nov 21, 2024
c8a26b3
Update diff print
zhong-al Nov 21, 2024
0d9ef0b
Try comparing using pandas
zhong-al Nov 21, 2024
236d015
Fix sep
zhong-al Nov 21, 2024
7c1d6ff
Check repr
zhong-al Nov 22, 2024
d0a2f5b
Check problem frame tensor
zhong-al Nov 22, 2024
758c4be
Try to force determinism
zhong-al Nov 22, 2024
d153a71
Merge branch 'master' into enhancement/model
zhong-al Dec 10, 2024
6ead110
Delete videos
zhong-al Dec 10, 2024
70843fa
Re-add a space
zhong-al Dec 10, 2024
9aff8aa
Fix merge issue
zhong-al Dec 10, 2024
068cda9
Try macos
zhong-al Dec 10, 2024
0026dd4
Fix merge issue
zhong-al Dec 10, 2024
d80112e
Fix config + example
zhong-al Dec 11, 2024
7a313c8
Apply suggestions from code review
zhong-al Dec 14, 2024
1aee502
Apply suggestion
zhong-al Dec 14, 2024
090679a
Add windows test
zhong-al Dec 14, 2024
1cb3bd0
Rename + add ubuntu workflow
zhong-al Dec 14, 2024
c5d4974
Update cvat2slowfast paths
zhong-al Dec 23, 2024
e3c52e5
Fix tool import
zhong-al Dec 23, 2024
a8f113c
Fix join_paths arg
zhong-al Dec 23, 2024
7dcfa1e
Uncomment tests
zhong-al Dec 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: macos test

on:
push:

jobs:
build:
runs-on: macos-latest
strategy:
matrix:
python-version: ["3.10"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
python -m pip install hatchling
python -m pip install --no-build-isolation .
- name: Running unit tests
env:
HF_TOKEN: ${{ secrets.HF_TOKEN_TESTING }}
run: |
python -m unittest tests/test_cvat2slowfast.py
python -m unittest tests/test_cvat2ultralytics.py
python -m unittest tests/test_detector2cvat.py
python -m unittest tests/test_miniscene2behavior.py
python -m unittest tests/test_player.py
python -m unittest tests/test_tracks_extractor.py
4 changes: 2 additions & 2 deletions .github/workflows/test.yml → .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Test
name: ubuntu test

on:
push:
Expand Down Expand Up @@ -31,4 +31,4 @@ jobs:
python -m unittest tests/test_detector2cvat.py
python -m unittest tests/test_miniscene2behavior.py
python -m unittest tests/test_player.py
python -m unittest tests/test_tracks_extractor.py
python -m unittest tests/test_tracks_extractor.py
34 changes: 34 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: windows test

on:
push:

jobs:
build:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.10"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
python -m pip install hatchling
python -m pip install --no-build-isolation .
- name: Running unit tests
env:
HF_TOKEN: ${{ secrets.HF_TOKEN_TESTING }}
run: |
python -m unittest tests/test_cvat2slowfast.py
python -m unittest tests/test_cvat2ultralytics.py
python -m unittest tests/test_detector2cvat.py
python -m unittest tests/test_miniscene2behavior.py
python -m unittest tests/test_player.py
python -m unittest tests/test_tracks_extractor.py
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ pip install git+https://github.com/Imageomics/kabr-tools

**Notes:**
- Refer to [pytorch.org](https://pytorch.org/get-started/locally/) to install specific versions of torch/CUDA
- [detectron2](https://detectron2.readthedocs.io/en/latest/tutorials/install.html#requirements) requires Linux or MacOS.
- If building detectron2's wheel fails, check gcc & g++ ≥ 5.4 (run `gcc --version` and `g++ --version`).
- SlowFast's setup.py is outdated; our workaround is `pip install git+https://github.com/Imageomics/SlowFast@797a6f3ae81c49019d006296f1e0f84f431dc356`, which is included when installing `kabr_tools`.

Each KABR tool can be run through the command line (as described below) or imported as a python module. They each have help information which can be accessed on the command line through `<tool-name> -h`.

Expand Down Expand Up @@ -95,17 +92,26 @@ tracks_extractor --video path_to_videos --annotation path_to_annotations [--trac
## Step 3: Label mini-scenes with behavior
You can use the [KABR model](https://huggingface.co/imageomics/x3d-kabr-kinetics) to label the mini-scenes with behavior. See the [ethogram](ethogram) folder for the list of behaviors used to label the zebra videos.


To use the [KABR model](https://huggingface.co/imageomics/x3d-kabr-kinetics), download `checkpoint_epoch_00075.pyth.zip`, unzip `checkpoint_epoch_00075.pyth`, and install [SlowFast](https://github.com/facebookresearch/SlowFast). Then run [miniscene2behavior.py](miniscene2behavior.py).

Label the mini-scenes:
```
miniscene2behavior [--config path_to_config] --checkpoint path_to_checkpoint [--gpu_num number_of_gpus] --miniscene path_to_miniscene [--output path_to_output_csv]
miniscene2behavior [--hub huggingface_hub] [--config path_to_config] --checkpoint path_to_checkpoint [--gpu_num number_of_gpus] --miniscene path_to_miniscene [--output path_to_output_csv]
```

**Examples:**
- download checkpoint from huggingface and extract config ex:
```
miniscene2behavior --hub imageomics/x3d-kabr-kinetics --checkpoint checkpoint_epoch_00075.pyth.zip --miniscene path_to_miniscene
```
- download checkpoint and config from huggingface ex:
```
miniscene2behavior --hub imageomics/x3d-kabr-kinetics --config config.yml --checkpoint checkpoint_epoch_00075.pyth --miniscene path_to_miniscene
```
- use local checkpoint and config ex:
```
miniscene2behavior --config config.yml --checkpoint checkpoint_epoch_00075.pyth --miniscene path_to_miniscene
```

**Notes:**
- If the config hasn't been extracted yet, the script will write it to `config`.
- `checkpoint` should be the path to `checkpoint_epoch_00075.pyth`.
- If `gpu_num` is 0, the model will use CPU. Using at least 1 GPU greatly increases inference speed. If you're using OSC, you can request a node with one GPU by running `sbatch -N 1 --gpus-per-node 1 -A [account] --time=[minutes] [bash script]`.
- mini-scenes are clipped videos focused on individual animals and video is the raw video file from which mini-scenes have been extracted.

Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ dependencies = [
"lxml",
"tqdm",
"torch",
"fvcore",
"natsort",
"ruamel.yaml",
"ultralytics",
"pandas",
"detectron2 @ git+https://github.com/facebookresearch/detectron2.git@2a420edb307c9bdf640f036d3b196bed474b8593",
"pytorchvideo @ git+https://github.com/facebookresearch/pytorchvideo.git@1fadaef40dd393ca09680f55582399f4679fc9b7",
"slowfast @ git+https://github.com/Imageomics/SlowFast@797a6f3ae81c49019d006296f1e0f84f431dc356"
"huggingface_hub",
"pytorchvideo @ git+https://github.com/facebookresearch/pytorchvideo.git@1fadaef40dd393ca09680f55582399f4679fc9b7"
]
keywords = [
"annotation",
Expand Down
29 changes: 17 additions & 12 deletions src/kabr_tools/cvat2slowfast.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pandas as pd
from natsort import natsorted
import cv2
from kabr_tools.utils.path import join_paths


def cvat2slowfast(path_to_mini_scenes: str, path_to_new_dataset: str,
Expand All @@ -24,13 +25,16 @@ def cvat2slowfast(path_to_mini_scenes: str, path_to_new_dataset: str,
if not os.path.exists(path_to_new_dataset):
os.makedirs(path_to_new_dataset)

if not os.path.exists(f"{path_to_new_dataset}/annotation"):
os.makedirs(f"{path_to_new_dataset}/annotation")
annotation_path = join_paths(path_to_new_dataset, "annotation")
if not os.path.exists(annotation_path):
os.makedirs(annotation_path)

if not os.path.exists(f"{path_to_new_dataset}/dataset/image"):
os.makedirs(f"{path_to_new_dataset}/dataset/image")
image_path = join_paths(path_to_new_dataset, "dataset", "image")
if not os.path.exists(image_path):
os.makedirs(image_path)

with open(f"{path_to_new_dataset}/annotation/classes.json", "w") as file:
classes_path = join_paths(annotation_path, "classes.json")
with open(classes_path, "w", encoding="utf-8") as file:
json.dump(label2number, file)

headers = {"original_vido_id": [], "video_id": pd.Series(dtype="int"), "frame_id": pd.Series(dtype="int"),
Expand All @@ -41,11 +45,12 @@ def cvat2slowfast(path_to_mini_scenes: str, path_to_new_dataset: str,
flag = False

for i, folder in enumerate(natsorted(os.listdir(path_to_mini_scenes))):
if os.path.exists(f"{path_to_mini_scenes}/{folder}/actions"):
for j, file in enumerate(natsorted(os.listdir(f"{path_to_mini_scenes}/{folder}/actions"))):
actions_path = join_paths(path_to_mini_scenes, folder, "actions")
if os.path.exists(actions_path):
for j, file in enumerate(natsorted(os.listdir(actions_path))):
if os.path.splitext(file)[1] == ".xml":
annotation_file = f"{path_to_mini_scenes}/{folder}/actions/{file}"
video_file = f"{path_to_mini_scenes}/{folder}/{os.path.splitext(file)[0]}.mp4"
annotation_file = join_paths(actions_path, file)
video_file = join_paths(path_to_mini_scenes, folder, f"{os.path.splitext(file)[0]}.mp4")

if not os.path.exists(video_file):
print(f"{video_file} does not exist.")
Expand Down Expand Up @@ -91,7 +96,7 @@ def cvat2slowfast(path_to_mini_scenes: str, path_to_new_dataset: str,

folder_code = f"{label[0].capitalize()}{folder_name:04d}"
folder_name += 1
output_folder = f"{path_to_new_dataset}/dataset/image/{folder_code}"
output_folder = join_paths(image_path, folder_code)
progress = f"{i + 1}/{len(os.listdir(path_to_mini_scenes))}," \
f"{j + 1}/{len(os.listdir(f'{path_to_mini_scenes}/{folder}/actions'))}:" \
f"{folder}/actions/{file} -> {output_folder}"
Expand Down Expand Up @@ -146,10 +151,10 @@ def cvat2slowfast(path_to_mini_scenes: str, path_to_new_dataset: str,

if video_id % 10 == 0:
charades_df.to_csv(
f"{path_to_new_dataset}/annotation/data.csv", sep=" ", index=False)
join_paths(annotation_path, "data.csv"), sep=" ", index=False)

charades_df.to_csv(
f"{path_to_new_dataset}/annotation/data.csv", sep=" ", index=False)
join_paths(annotation_path, "data.csv"), sep=" ", index=False)


def parse_args() -> argparse.Namespace:
Expand Down
131 changes: 77 additions & 54 deletions src/kabr_tools/miniscene2behavior.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,33 @@
import sys
import argparse
import random
from zipfile import ZipFile
import torch
from lxml import etree
import numpy as np
import pandas as pd
import cv2
from tqdm import tqdm
import slowfast.utils.checkpoint as cu
from slowfast.models import build
from slowfast.utils import parser
from slowfast.datasets.utils import get_sequence
from slowfast.visualization.utils import process_cv2_inputs
from slowfast.datasets.cv2_transform import scale
from fvcore.common.config import CfgNode
from torch import Tensor


def get_input_clip(cap: cv2.VideoCapture, cfg: CfgNode, keyframe_idx: int) -> list[Tensor]:
# https://github.com/facebookresearch/SlowFast/blob/bac7b672f40d44166a84e8c51d1a5ba367ace816/slowfast/visualization/ava_demo_precomputed_boxes.py
seq_length = cfg.DATA.NUM_FRAMES * cfg.DATA.SAMPLING_RATE
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
seq = get_sequence(
keyframe_idx,
seq_length // 2,
cfg.DATA.SAMPLING_RATE,
total_frames,
)
clip = []
for frame_idx in seq:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
was_read, frame = cap.read()
if was_read:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = scale(cfg.DATA.TEST_CROP_SIZE, frame)
clip.append(frame)
else:
print("Unable to read frame. Duplicating previous frame.")
clip.append(clip[-1])

clip = process_cv2_inputs(clip, cfg)
return clip
from huggingface_hub import hf_hub_download
from kabr_tools.utils.slowfast.utils import get_input_clip
from kabr_tools.utils.slowfast.cfg import load_config, CfgNode
from kabr_tools.utils.slowfast.x3d import build_model


def get_cached_datafile(repo_id: str, filename: str):
return hf_hub_download(repo_id=repo_id, filename=filename)


def parse_args() -> argparse.Namespace:
local_parser = argparse.ArgumentParser()
local_parser.add_argument(
"--hub",
type=str,
help="model hub name"
)
local_parser.add_argument(
"--config",
type=str,
help="model config.yml filepath",
default="config.yml"
help="model config.yml filepath"
)
local_parser.add_argument(
"--checkpoint",
Expand Down Expand Up @@ -84,24 +64,28 @@ def parse_args() -> argparse.Namespace:


def create_model(config_path: str, checkpoint_path: str, gpu_num: int) -> tuple[CfgNode, torch.nn.Module]:
# load model config
try:
cfg = parser.load_config(parser.parse_args(), config_path)
except FileNotFoundError:
checkpoint = torch.load(
checkpoint_path, map_location=torch.device("cpu"))
with open(config_path, "w") as file:
file.write(checkpoint["cfg"])
cfg = parser.load_config(parser.parse_args(), config_path)
cfg.NUM_GPUS = gpu_num
cfg.OUTPUT_DIR = ""
model = build.build_model(cfg)
# check params
assert config_path is not None
assert checkpoint_path is not None
assert gpu_num >= 0

# load model checkpoint
cu.load_checkpoint(checkpoint_path, model, data_parallel=False)
# load config
cfg = load_config(config_path)
cfg.NUM_GPUS = gpu_num

# set model to eval mode
# set random seed
random.seed(cfg.RNG_SEED)
np.random.seed(cfg.RNG_SEED)
torch.manual_seed(cfg.RNG_SEED)
torch.use_deterministic_algorithms(True)

# load model
model = build_model(cfg)
checkpoint = torch.load(checkpoint_path, weights_only=True,
map_location=torch.device("cpu"))
model.load_state_dict(checkpoint["model_state"])
model.eval()

return cfg, model


Expand Down Expand Up @@ -151,6 +135,8 @@ def annotate_miniscene(cfg: CfgNode, model: torch.nn.Module,
inputs = inputs.cuda(non_blocking=True)

preds = model(inputs)
if frame == 1:
print(preds)
preds = preds.detach()

if cfg.NUM_GPUS:
Expand All @@ -166,10 +152,47 @@ def annotate_miniscene(cfg: CfgNode, model: torch.nn.Module,
pd.DataFrame(label_data).to_csv(output_path, sep=" ", index=False)


def download_model(args) -> None:
# download checkpoint from huggingface
args.checkpoint = get_cached_datafile(args.hub, args.checkpoint)
checkpoint_folder = args.checkpoint.rsplit("/", 1)[0]

# extract checkpoint archive
if args.checkpoint.rsplit(".", 1)[-1] == "zip":
with ZipFile(args.checkpoint, "r") as zip_ref:
zip_ref.extractall(checkpoint_folder)
args.checkpoint = args.checkpoint.rsplit(".", 1)[0]

# download config from huggingface
if args.config:
args.config = get_cached_datafile(args.hub, args.config)


def extract_config(args) -> None:
# extract config from checkpoint
if len(args.checkpoint.rsplit("/", 1)) > 1:
checkpoint_folder = args.checkpoint.rsplit("/", 1)[0]
else:
checkpoint_folder = "."

checkpoint = torch.load(args.checkpoint,
map_location=torch.device("cpu"),
weights_only=True)
config_path = f"{checkpoint_folder}/config.yml"
with open(config_path, "w", encoding="utf-8") as file:
file.write(checkpoint["cfg"])
args.config = config_path


def main() -> None:
# clear arguments to avoid slowfast parsing issues
args = parse_args()
sys.argv = [sys.argv[0]]

if args.hub:
download_model(args)

if not args.config:
extract_config(args)

cfg, model = create_model(args.config, args.checkpoint, args.gpu_num)
annotate_miniscene(cfg, model, args.miniscene,
args.video, args.output)
Expand Down
6 changes: 6 additions & 0 deletions src/kabr_tools/utils/path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pathlib import Path


def join_paths(*parts):
assert len(parts) > 0, "At least one path must be provided"
return str(Path(*parts))
Loading
Loading