LabGym opsætning og brug på AI-LAB

Generel guide: Sådan kører du LabGym på AI-LAB

Denne guide viser, hvordan du sætter et generelt LabGym-projekt op på AI-LAB, så det kan køre headless (uden GUI) og som SLURM array jobs på GPU. Guiden er skrevet, så den ikke kun passer til ét bestemt dyr eller ét bestemt specialeprojekt.

Undervejs vil der stå DITBRUGERNAVN. Det betyder dit grimme AAU-brugernavn uden @student.aau.dk.


Hvad denne guide antager

Denne guide antager, at du allerede har:

Hvis du mangler modeller, skal de først trænes i LabGym lokalt.

Vigtige placeholders du selv skal ændre

Du skal selv ændre disse steder i guiden og de forskellige scripts:

Ressourcer på AI-LAB: vælg kun det du har brug for

Værdier som --mem, --cpus-per-task, --time, antal samtidige array-jobs og brug af GPU skal optimeres til det konkrete projekt.

Brug ikke flere ressourcer end du har behov for.

Det afhænger blandt andet af:

Start med små testvideoer og justér derefter.

Praktisk tommelfingerregel

Eksempel: --mem=80G og --cpus-per-task=8 er kun et eksempel, ikke et universelt krav.

Opsætning

TRIN 1 — Log ind på AI-LAB

Åbn PowerShell (Windows) eller Terminal (macOS) og skriv:

ssh ailab-1

eller

ssh ailab-2

alt efter hvad der virkede under opsætning af AI-LAB

TRIN 2 — Opret mapper

Når du er logget ind:

mkdir -p ~/labgym_project/{code,data/videos,data/models,results,logs,tmp}

labgym_project kan ændres til et mere relevant navn for dit projekt. Men HUSK at ændre det i resten af guiden også!

Mappestrukturen i dit projekt bliver:

~/labgym_project/  
├── code/           # scripts
├── data/  
│   ├── videos/
│   └── models/     # detector og categorizer  
├── results/        # output
└── logs/           # SLURM logs
└── tmp/            # midlertidige filer

Læg derefter:

TRIN 3 — Opret virtual environment i Python-containeren

Kør:

srun --mem=8G --cpus-per-task=2 \
singularity exec /ceph/container/python/python_3.10.sif \
python -m venv --system-site-packages ~/labgym_project/venv

Dette laver et virtual python environment, som bruges sammen med AI-LABs Python-container.

TRIN 4 — Hent LabGym-koden

Gå til kode mappen:

cd ~/labgym_project/code

Hent LabGym source code fra GitHub:

git clone https://github.com/umyelab/LabGym.git

TRIN 5 — Fjern GUI-afhængigheden

LabGym er normalt lavet til GUI-brug. På AI-LAB vil vi køre headless (uden GUI), så wxPython skal fjernes fra dependencies.

Åben pyproject.toml i LabGym:

nano ~/labgym_project/code/LabGym/pyproject.toml

Find wxPython i dependencies og fjern den.
Gem (Ctrl+s) og luk (Ctrl+x)

TRIN 6 — Installér LabGym i venv inde i containeren

Start interaktivt job:

srun --mem=24G --cpus-per-task=8 --time=02:00:00 --pty bash

Kør derefter installationen:

singularity exec \
-B ~/labgym_project:/scratch/labgym_project \
/ceph/container/python/python_3.10.sif \
/bin/bash -c "
source /scratch/labgym_project/venv/bin/activate

pip uninstall -y labgym tensorflow numpy opencv-python opencv-contrib-python opencv-python-headless

pip install --no-cache-dir numpy==1.26.4

pip install --no-cache-dir tensorflow==2.15.1

pip install --no-cache-dir opencv-python-headless==4.10.0.84

cd /scratch/labgym_project/code/LabGym

pip install -e . --no-deps
"

Bemærk
Der VIL komme warnings og errors når vi installere, tester og bruger LabGym, da LabGym ikke er lavet til at blive brugt på denne måde.

TRIN 7 — Test at installationen virker

Kør:

singularity exec --nv \
-B ~/labgym_project:/scratch/labgym_project \
/ceph/container/python/python_3.10.sif \
/bin/bash -c "
source /scratch/labgym_project/venv/bin/activate
python - <<'PY'
import numpy
print('numpy:', numpy.__version__)

import cv2
print('cv2 works')

import torch
print('torch:', torch.__version__)
print('torch cuda available:', torch.cuda.is_available())

from LabGym.analyzebehavior_dt import AnalyzeAnimalDetector
print('OK - LabGym detector API works')
PY
"

Hvis der til sidst står:

OK - LabGym detector API works

så virker installationen selv om der kommer mangle advarsler og fejl.

Når du er færdig med det interaktive job, kan du lukke det med:

scancel JOBID

Job-id kan findes med denne kode hvis du ikke kan huske den fra da du startede det interaktive job:

squeue --me

TRIN 8 — Opret Python-wrapper-script

Formålet med wrapper-scriptet er at gøre LabGym nemmere at kalde fra SLURM scripts og samtidig gøre det muligt at styre parametre fra kommandolinjen.

Lav python filen run_labgym_detector.py ved brug af koden:

nano ~/labgym_project/code/run_labgym_detector.py

Indsæt dette i filen:

import argparse
import ast
import csv
from pathlib import Path

from LabGym.analyzebehavior_dt import AnalyzeAnimalDetector


def parse_animal_number(raw_value, animal_kinds):
    raw_value = str(raw_value).strip()

    if raw_value.isdigit():
        return int(raw_value)

    parsed = ast.literal_eval(raw_value)

    if isinstance(parsed, int):
        return parsed

    if isinstance(parsed, dict):
        return {str(k): int(v) for k, v in parsed.items()}

    if isinstance(parsed, (list, tuple)):
        if len(parsed) != len(animal_kinds):
            raise ValueError(
                f"--animal-number as a list must have the same length as --animal-kinds. "
                f"Got {len(parsed)} values but {len(animal_kinds)} classes."
            )
        return {animal_kinds[i]: int(parsed[i]) for i in range(len(animal_kinds))}

    raise ValueError("--animal-number must be an integer, dict, or list.")


def read_categorizer_table(categorizer_dir):
    mp = Path(categorizer_dir) / "model_parameters.txt"
    if not mp.exists():
        raise FileNotFoundError(f"Could not find model_parameters.txt in {categorizer_dir}")

    with mp.open("r", encoding="utf-8", errors="ignore", newline="") as f:
        reader = csv.DictReader(f)
        rows = list(reader)

    if not rows:
        raise ValueError("model_parameters.txt exists but contains no rows.")

    return rows


def build_names_and_colors(behavior_names):
    default_pairs = [
        ["#ffffff", "#ff00ff"],
        ["#ffffff", "#00ffff"],
        ["#ffffff", "#00ff00"],
        ["#ffffff", "#ff9900"],
        ["#ffffff", "#ff0000"],
        ["#ffffff", "#0000ff"],
        ["#ffffff", "#9900ff"],
        ["#ffffff", "#999999"],
    ]

    names_and_colors = {}
    for i, name in enumerate(behavior_names):
        names_and_colors[name] = default_pairs[i % len(default_pairs)]
    return names_and_colors


def build_id_colors(animal_number):
    default_colors = [
        (255, 255, 255),
        (255, 0, 0),
        (0, 255, 0),
        (0, 0, 255),
        (255, 255, 0),
        (255, 0, 255),
        (0, 255, 255),
        (255, 128, 0),
        (128, 0, 255),
        (128, 128, 128),
    ]

    if isinstance(animal_number, int):
        total = animal_number
    elif isinstance(animal_number, dict):
        total = sum(animal_number.values())
    else:
        raise ValueError("animal_number must be int or dict")

    return [default_colors[i % len(default_colors)] for i in range(total)]


def first_int(rows, key, default):
    value = rows[0].get(key, default)
    try:
        return int(value)
    except Exception:
        return default


def main():
    parser = argparse.ArgumentParser()

    # Krævede argumenter
    parser.add_argument("--video", required=True)
    parser.add_argument("--detector", required=True)
    parser.add_argument("--categorizer", required=True)
    parser.add_argument("--results", required=True)
    parser.add_argument("--animal-number", required=True)
    parser.add_argument("--animal-kinds", nargs="+", required=True)

    # Analyseindstillinger
    parser.add_argument("--behavior-mode", type=int, default=None)
    parser.add_argument("--framewidth", type=int, default=0)
    parser.add_argument("--dim-tconv", type=int, default=None)
    parser.add_argument("--dim-conv", type=int, default=None)
    parser.add_argument("--channel", type=int, default=None)
    parser.add_argument("--include-bodyparts", action="store_true")
    parser.add_argument("--std", type=int, default=None)
    parser.add_argument("--start-time", type=float, default=0)
    parser.add_argument("--duration", type=float, default=0)
    parser.add_argument("--length", type=int, default=None)
    parser.add_argument("--social-distance", type=int, default=None)

    # Performance / inputbehandling
    parser.add_argument("--batch-size", type=int, default=1)
    parser.add_argument("--background-free", action="store_true")

    # Behavior categorization
    parser.add_argument("--uncertain", type=int, default=0)
    parser.add_argument("--min-behavior-length", type=int, default=None)

    # Annoteret video
    parser.add_argument("--skip-annotated-video", action="store_true")
    parser.add_argument("--hide-legend", action="store_true")

    # Resultateksport
    parser.add_argument("--no-normalize-distance", action="store_true")
    parser.add_argument(
        "--parameters",
        nargs="+",
        default=["count", "duration"],
        help="Fx count duration latency speed distance intensity area ..."
    )

    args = parser.parse_args()

    uncertain_value = args.uncertain / 100.0

    results = Path(args.results)
    results.mkdir(parents=True, exist_ok=True)

    animal_number = parse_animal_number(args.animal_number, args.animal_kinds)

    rows = read_categorizer_table(args.categorizer)

    behavior_names = [row["classnames"] for row in rows if row.get("classnames")]
    if not behavior_names:
        raise ValueError("Could not extract behavior names from the 'classnames' column.")

    dim_tconv = args.dim_tconv if args.dim_tconv is not None else first_int(rows, "dim_tconv", 8)
    dim_conv = args.dim_conv if args.dim_conv is not None else first_int(rows, "dim_conv", 8)
    channel = args.channel if args.channel is not None else first_int(rows, "channel", 1)
    length = args.length if args.length is not None else first_int(rows, "time_step", 15)
    std = args.std if args.std is not None else first_int(rows, "std", 0)
    behavior_mode = args.behavior_mode if args.behavior_mode is not None else first_int(rows, "behavior_kind", 0)
    social_distance = args.social_distance if args.social_distance is not None else first_int(rows, "social_distance", 0)
    network = first_int(rows, "network", 1)
    animation_analyzer = network != 0

    names_and_colors = build_names_and_colors(behavior_names)
    id_colors = build_id_colors(animal_number)

    print("Parsed animal_number:", animal_number)
    print("Animal kinds:", args.animal_kinds)
    print("Behavior names:", behavior_names)
    print("dim_tconv:", dim_tconv)
    print("dim_conv:", dim_conv)
    print("channel:", channel)
    print("length:", length)
    print("behavior_mode:", behavior_mode)
    print("social_distance:", social_distance)
    print("ID colors:", id_colors)

    aad = AnalyzeAnimalDetector()

    aad.prepare_analysis(
        args.detector,
        args.video,
        args.results,
        animal_number,
        args.animal_kinds,
        behavior_mode,
        names_and_colors=names_and_colors,
        framewidth=None if args.framewidth == 0 else args.framewidth,
        dim_tconv=dim_tconv,
        dim_conv=dim_conv,
        channel=channel,
        include_bodyparts=args.include_bodyparts,
        std=std,
        categorize_behavior=True,
        animation_analyzer=animation_analyzer,
        t=args.start_time,
        duration=args.duration,
        length=length,
        social_distance=social_distance,
    )

    aad.acquire_information(
        batch_size=args.batch_size,
        background_free=args.background_free
    )

    if behavior_mode != 1:
        aad.craft_data()

    aad.categorize_behaviors(
        args.categorizer,
        uncertain=args.uncertain,
        min_length=args.min_behavior_length
    )

    if not args.skip_annotated_video:
        aad.annotate_video(
            ID_colors=id_colors,
            animal_to_include=args.animal_kinds,
            behavior_to_include=behavior_names,
            show_legend=not args.hide_legend
        )

    aad.export_results(
        normalize_distance=not args.no_normalize_distance,
        parameter_to_analyze=args.parameters
    )

    print("Done.")


if __name__ == "__main__":
    main()

Gem (Ctrl+s) og luk (Ctrl+x)

TRIN 9 — Lav array job script

Videoerne til test analyse skal nu ligge inde i mappen til videoer. Til at starte med kan man have et par meget korte test videoer.

Lav en array script:

nano ~/labgym_project/code/run_labgym_array.sh

Indsæt:

#!/bin/bash
#SBATCH --job-name=labgym_project
#SBATCH --output=/ceph/home/student.aau.dk/DITBRUGERNAVN/labgym_project/logs/labgym_%A_%a.out
#SBATCH --error=/ceph/home/student.aau.dk/DITBRUGERNAVN/labgym_project/logs/labgym_%A_%a.err

# Justér disse ressourcer til dit projekt
#SBATCH --mem=80G
#SBATCH --cpus-per-task=8
#SBATCH --gres=gpu:1
#SBATCH --time=08:00:00

set -euo pipefail

BASE=/ceph/home/student.aau.dk/DITBRUGERNAVN/labgym_project
VIDEO_LIST=${BASE}/code/video_list.txt

FILE=$(sed -n "$((SLURM_ARRAY_TASK_ID + 1))p" "${VIDEO_LIST}")

if [ -z "${FILE}" ]; then
    echo "No video found for task ${SLURM_ARRAY_TASK_ID}"
    exit 1
fi

BASENAME=$(basename "${FILE}")
BASENAME_NOEXT="${BASENAME%.*}"
RESULTS_DIR=/scratch/labgym_project/results/${BASENAME_NOEXT}

singularity exec --nv \
-B ${BASE}:/scratch/labgym_project \
/ceph/container/python/python_3.10.sif \
/bin/bash -c "
source /scratch/labgym_project/venv/bin/activate

export TMPDIR=/scratch/labgym_project/tmp
export TEMP=/scratch/labgym_project/tmp
export TMP=/scratch/labgym_project/tmp

mkdir -p /scratch/labgym_project/tmp
mkdir -p '${RESULTS_DIR}'

python /scratch/labgym_project/code/run_labgym_detector.py \
  --video '${FILE}' \
  --detector /scratch/labgym_project/data/models/DETector_MAPPE \
  --categorizer /scratch/labgym_project/data/models/CATEGORIZER_MAPPE \
  --results '${RESULTS_DIR}' \
  --animal-number '{\"Animal\": 1}' \
  --animal-kinds Animal \
  --batch-size 1 \
  --duration 0 \
  --parameters count duration

  # ============================
  # Mulige ekstra indstillinger
  # Fjern # foran dem du vil bruge
  # ============================

  # --behavior-mode 0 \
  # --framewidth 0 \
  # --dim-tconv 8 \
  # --dim-conv 8 \
  # --channel 1 \
  # --include-bodyparts \
  # --std 0 \
  # --start-time 0 \
  # --duration 60 \
  # --length 15 \
  # --social-distance 0 \
  # --batch-size 2 \
  # --background-free \
  # --uncertain 20 \
  # --min-behavior-length 10 \
  # --hide-legend \
  # --no-normalize-distance \
  # --skip-annotated-video \
"

Ting du altid skal ændre for at passe til dit projekt

Eksempler på --animal-number

Én klasse:

--animal-number '{"Animal": 3}' \
--animal-kinds Animal

Flere klasser:

--animal-number '{"Male": 1, "Female": 2}' \
--animal-kinds Male Female

Hvis du bruger flere klasser, skal navnene i --animal-number og --animal-kinds passe sammen.

Forklaring af LabGym-argumenterne i array-scriptet

Krævede argumenter
Analyseindstillinger

Dette er de indstillinger der skal passe med den trænet model

Performance / inputbehandling

Behavior categorization

Annoteret video

Resultateksport

Hvis du vil eksportere andre eller flere mål, kan du skrive fx:

--parameters count duration latency speed distance

Hvilke parametre der giver mening, afhænger af dit LabGym-setup og hvad export_results forventer i den version af LabGym, du bruger.

TRIN 10 — Opret submit script

Lav filen:

nano ~/labgym_project/code/submit_array.sh

Indsæt:

#!/bin/bash

PROJECT_DIR=$HOME/labgym_project
INPUT_DIR=/ceph/home/student.aau.dk/DITBRUGERNAVN/video_processing/data_out
FILE_LIST=$PROJECT_DIR/code/video_list.txt
JOB_SCRIPT=$PROJECT_DIR/code/run_labgym_array.sh

mkdir -p "$PROJECT_DIR/logs"
mkdir -p "$PROJECT_DIR/results"
mkdir -p "$PROJECT_DIR/tmp"

find "$INPUT_DIR" -maxdepth 1 -type f -name "*.mp4" | sort > "$FILE_LIST"

NUM_FILES=$(wc -l < "$FILE_LIST")

if [ "$NUM_FILES" -eq 0 ]; then
    echo "No MP4 files found in $INPUT_DIR"
    exit 1
fi

MAX_INDEX=$((NUM_FILES - 1))

echo "Found $NUM_FILES videos"
echo "Submitting jobs"

# %8 betyder maks 8 samtidige array tasks
sbatch --array=0-"$MAX_INDEX"%8 "$JOB_SCRIPT"

Vigtigt

TRIN 11 — Gør scripts kørbare

chmod +x ~/labgym_project/code/run_labgym_array.sh
chmod +x ~/labgym_project/code/submit_array.sh

Brug LabGym

Sådan kører man array-jobbet

Sådan sender du array-jobbet

sbatch ~/labgym_project/code/submit_array.sh

Sådan tjekker du køen

squeue --me

Hvor finder du output?

Hver video får sin egen resultatmappe:

~/labgym_project/results/video_navn/

hver array-task får sin egen log:

~/labgym_project/logs/labgym_JOBID_TASKID.out  
~/labgym_project/logs/labgym_JOBID_TASKID.err

Få videoer over i .mp4 med ffmpeg

Hvis dine videoer ikke allerede er i .mp4, kan du konvertere dem med ffmpeg.

Simpel konvertering til MP4 i python terminal

Hvis du bare vil lave en ny MP4-fil:

ffmpeg -i "input.avi" "output.mp4"

Erstat input.avi med stien til den video du gerne vil ændre til mp4.
Erstat output.mp4 med stien og det nye navn til videoen

For eksempel:

ffmpeg -i "E:\1_01_H_20251226000000.avi" "E:\1_01_H_20251226000000.mp4"

ffmpeg har mange indstillinger og jeg vil anbefale at spørge ChatGPT (eller anden chatbot) om yderligere anbefalinger og koder til konvertering af videoer til dit projekt

Fejlsøgning

Job fejler med memory error

Job kører meget langsomt

Ingen outputfiler

Powered by Forestry.md