diff --git a/Manifest.toml b/Manifest.toml index 990bfdaa0182ec5e16aa3c3334342906352bf80b..e16f846d4be150f861b34a059f113ac47eec7797 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -954,11 +954,11 @@ version = "0.3.2" [[deps.PlanarLarvae]] deps = ["DelimitedFiles", "HDF5", "JSON3", "LinearAlgebra", "MAT", "Meshes", "OrderedCollections", "Random", "SHA", "StaticArrays", "Statistics", "StatsBase", "StructTypes"] -git-tree-sha1 = "a6ced965b03efe596835f093d0ffbf1e8d991d50" -repo-rev = "v0.14a2" -repo-url = "https://gitlab.pasteur.fr/nyx/planarlarvae.jl" +git-tree-sha1 = "6b2dc28d56bcef101672cbf2bb784bbd5d88d579" +repo-rev = "main" +repo-url = "https://gitlab.pasteur.fr/nyx/PlanarLarvae.jl" uuid = "c2615984-ef14-4d40-b148-916c85b43307" -version = "0.14.0-a" +version = "0.15.0" [[deps.PlotUtils]] deps = ["ColorSchemes", "Colors", "Dates", "PrecompileTools", "Printf", "Random", "Reexport", "Statistics"] diff --git a/Project.toml b/Project.toml index 915301c48800ec0d4b2eb09ac64597c25d2215ca..a62f34aff4b9a40aba0ee45f68af67d672472fc2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "LarvaTagger" uuid = "8b3b36f1-dfed-446e-8561-ea19fe966a4d" authors = ["François Laurent", "Institut Pasteur"] -version = "0.16.4" +version = "0.17" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" diff --git a/README.md b/README.md index 84d9df3189dbda6fc4b491ae840ed01b682a835c..73d662204d9a4e11b62e22ca3e060e5207f27ee0 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,13 @@ julia> using LarvaTagger; display(larvaeditor("path/to/data/file")) To exit the interpreter, type `exit()` or press Ctrl+D. +### macOS + +On macOS computers, the 2D larva view often shows up twice as small as expected. To mitigate this undesired behavior, +`larvatagger open` admits a `--view-factor` option, and `larvaeditor` admits a `viewfactor` argument. +This option/argument is 2 per default on macOS, 1 on the other platforms. +Feel free to adjust the value if the 2D view is too small or large. + ## Automatic tagging `LarvaTagger.jl` comes with no automatic tagger per default. @@ -125,6 +132,8 @@ and apply this tagger to a tracking data file: scripts/larvatagger predict <path/to/backend> <tagger-name> <path/to/data/file> ``` +Among the many optional arguments to the `train` command, an important one is `--iterations`. It allows specifying the training budget. In several applications, higher training scores were achieved increasing the value for this argument. The default for `MaggotUBA-adapter` tagging backend is 1000. `MaggotUBA-adapter` admits either a single value or a comma-separated pair of values. Indeed, `MaggotUBA-adapter` training is performed in two phases: first the classifier stage is trained, with static weights in the pretrained MaggotUBA encoder; second both the classifier and encoder are fine-tuned. A higher training budget for the second fine-tuning stage may significantly increase the training accuracy. + Note: since `TaggingBackends==0.10`, argument `--skip-make-dataset` is default behavior; pass `--make-dataset` instead to enforce the former default. To run `larvatagger predict` in parallel on multiple data files using the same tagger, append the `--data-isolation` argument to avoid data conflicts. diff --git a/recipes/Dockerfile b/recipes/Dockerfile index a8feb6caef384265c5615b8f83897b4133b24440..3c516f31810aef33875d8fe4b079669760bebb02 100644 --- a/recipes/Dockerfile +++ b/recipes/Dockerfile @@ -79,7 +79,7 @@ RUN if [ -z $TAGGINGBACKENDS_BRANCH ]; then \ && rm -rf .git \ && poetry install --only main \ && poetry add "pynvml==11.4.1" \ - && if [ "$(echo $BACKEND | cut -d/ -f2)" = "main" ] || [ "$(echo $BACKEND | cut -d/ -f2)" = "dev" ] || [ "$(echo $BACKEND | cut -d/ -f2)" = "debug" ]; then \ + && if [ "$(echo $BACKEND | cut -d/ -f2)" = "main" ] || [ "$(echo $BACKEND | cut -d/ -f2)" = "dev" ]; then \ julia -e 'using Pkg; Pkg.add("JSON3")' \ && scripts/make_models.jl default; \ fi \ @@ -88,3 +88,15 @@ RUN if [ -z $TAGGINGBACKENDS_BRANCH ]; then \ COPY recipes/checkgpu /bin/ + +FROM backend AS confusion + +ARG PYTHON_ENV=/app/MaggotUBA + +RUN test -d $PYTHON_ENV \ + && cd $PYTHON_ENV \ + && poetry add "scikit-learn==1.3.0" \ + && rm -rf ~/.cache + +ADD https://gitlab.pasteur.fr/nyx/TaggingBackends/-/raw/dev/scripts/confusion.py?ref_type=heads&inline=false /bin/confusion.py + diff --git a/recipes/README.md b/recipes/README.md index 99c3b8945187075c7e5a48070add3f6879e6f7ef..79be90e707a866ee1f8d1ebf945bf74ee65c8719 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -171,7 +171,7 @@ docker pull flaur/larvatagger ``` Beware that images that ship with backends are relatively large files (>5GB on disk). -If you are not interested in automatic tagging, use the `flaur/larvatagger:0.16.2-standalone` image instead. +If you are not interested in automatic tagging, use the `flaur/larvatagger:0.17-standalone` image instead. ### Upgrading diff --git a/scripts/larvatagger b/scripts/larvatagger index 4c7779ab51cb86d2beac1f77e6875cf3c835b8b0..d9a475be6a19e7f2cab8feccfc1feea31bd87bf4 100755 --- a/scripts/larvatagger +++ b/scripts/larvatagger @@ -29,36 +29,40 @@ import|merge|train|predict|--version|-V) LarvaTagger Usage: - larvatagger open <file-path> [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--manual-label=<label>] [--segment=<t0,t1>] + larvatagger open <file-path> [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--view-factor=<real>] [--manual-label=<label>] larvatagger import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>] [--pixelsize=<μm>] [--overrides=<comma-separated-list>] [--default-label=<label>] [--manual-label=<label>] [--decode] [--copy-labels] - larvatagger train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>] [--seed=<seed>] - larvatagger train <backend-path> <data-path> <model-instance> --fine-tune=<instance> [--balancing-strategy=<strategy>] [--manual-label=<label>] [--iterations=<N>] [--seed=<seed>] - larvatagger predict <backend-path> <model-instance> <data-path> [--output=<filename>] [--make-dataset] [--skip-make-dataset] [--data-isolation] + larvatagger train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>] [--seed=<seed>] [--debug] + larvatagger train <backend-path> <data-path> <model-instance> --fine-tune=<instance> [--balancing-strategy=<strategy>] [--manual-label=<label>] [--iterations=<N>] [--seed=<seed>] [--debug] + larvatagger predict <backend-path> <model-instance> <data-path> [--output=<filename>] [--make-dataset] [--skip-make-dataset] [--data-isolation] [--debug] + larvatagger predict <backend-path> <model-instance> <data-path> --embeddings [--data-isolation] [--debug] larvatagger merge <input-path> <input-file> [<output-file>] [--manual-label=<label>] [--decode] larvatagger -V | --version larvatagger -h | --help Options: - -h --help Show this screen. - -V --version Show version. - -q --quiet Do not show instructions. - --id=<id> Run or assay ID, e.g. `date_time`. - --framerate=<fps> Camera frame rate, in frames per second. - --pixelsize=<μm> Camera pixel size, in micrometers. - --backends=<path> Path to backend repository. - --port=<number> Port number the server listens to. - --viewer Disable editing capabilities. - --browser Automatically open a browser tab at the served location. - --make-dataset Perform the make_dataset stage prior to proceeding to predict_model. - --skip-make-dataset Skip the make_dataset stage (default behavior since TaggingBackends==0.10). - --data-isolation Isolate the backend data directories for parallel tagging of multiple data files. - --sample-size=<N> Sample only N track segments from the data repository. - --layers=<N> (MaggotUBA) Number of layers of the classifier. - --iterations=<N> (MaggotUBA) Number of training iterations (can be two integers separated by a comma). - --seed=<seed> Seed for the backend's random number generators. - --segment=<t0,t1> Start and end times (included, comma-separated) for cropping and including tracks. - --decode Do not encode the labels into integer indices. - --copy-labels Replicate discrete behavior data from the input file. + -h --help Show this screen. + -V --version Show version. + -q --quiet Do not show instructions. + --id=<id> Run or assay ID, e.g. `date_time`. + --framerate=<fps> Camera frame rate, in frames per second. + --pixelsize=<μm> Camera pixel size, in micrometers. + --backends=<path> Path to backend repository. + --port=<number> Port number the server listens to. + --viewer Disable editing capabilities. + --browser Automatically open a browser tab at the served location. + --view-factor=<real> Scaling factor for the larva views; default is 2 on macOS, 1 elsewhere. + --make-dataset Perform the make_dataset stage prior to proceeding to predict_model. + --skip-make-dataset Skip the make_dataset stage (default behavior since TaggingBackends==0.10). + --data-isolation Isolate the backend data directories for parallel tagging of multiple data files. + --sample-size=<N> Sample only N track segments from the data repository. + --layers=<N> (MaggotUBA) Number of layers of the classifier. + --iterations=<N> (MaggotUBA) Number of training iterations (can be two integers separated by a comma). + --seed=<seed> Seed for the backend's random number generators. + --segment=<t0,t1> Start and end times (included, comma-separated) for cropping and including tracks. + --debug Lower the logging level to DEBUG. + --embeddings (MaggotUBA) Call the backend to generate embeddings instead of labels. + --decode Do not encode the labels into integer indices. + --copy-labels Replicate discrete behavior data from the input file. --default-label=<label> Label all untagged data as <label>. --manual-label=<label> Secondary label for manually labelled data [default: edited]. --labels=<comma-separated-list> Comma-separated list of behavior tags/labels. diff --git a/scripts/larvatagger.sh b/scripts/larvatagger.sh index 885c5dc78adde2425b413ce922d42558a509ad0b..3222537602cecfdee7015b6f0c38f64c21d026b6 100755 --- a/scripts/larvatagger.sh +++ b/scripts/larvatagger.sh @@ -2,7 +2,7 @@ for _ in $(seq $#); do case $1 in - open|import|merge|train|predict|-V|--version|--more-help|reverse-mapping) + open|import|merge|train|predict|-V|--version|--more-help|reverse-mapping|confusion) cmd=$1 shift break @@ -29,6 +29,10 @@ for _ in $(seq $#); do if [ "$1" = "--no-cache" ]; then # --no-cache is default for build since 0.16.1 no_cache=1 + elif [ "$1" = "--target" ]; then + DOCKER_ARGS="${DOCKER_ARGS}$1 " + shift + target=1 fi # note: if DOCKER_ARGS is externally defined, it must end with an explicit space DOCKER_ARGS="${DOCKER_ARGS}$1 " @@ -95,7 +99,9 @@ done if [ -z "$no_cache" ] && [ -z "$cache" ]; then DOCKER_ARGS="--no-cache $DOCKER_ARGS" fi +if [ -z "$target" ]; then DOCKER_ARGS="--target $TARGET $DOCKER_ARGS" +fi if [ -z "$DOCKERFILE" ]; then DOCKERFILE=recipes/Dockerfile fi @@ -296,6 +302,22 @@ tagger="20230311" DOCKER_RUN="$docker run $DOCKER_ARGS$RUN_ARGS -it --entrypoint julia \"$LARVATAGGER_IMAGE\" \"/app/$backend/scripts/revert_label_mapping.jl\" \"/data/$output_labels\" \"/data/$unmapped_labels\" \"/data/$edited_labels\" \"/app/$backend/models/$tagger/clf_config.json\"" echo $DOCKER_RUN eval $DOCKER_RUN +;; + + confusion) + +parentdir=$(cd "$1"; pwd -P) +shift + +RUN_ARGS="$RUN_ARGS -v \"$parentdir\":/data" + +# wherever taggingbackends (python) is installed +backend=MaggotUBA + +DOCKER_RUN="$docker run $DOCKER_ARGS$RUN_ARGS -it -w /app/$backend --entrypoint='[\"poetry\", \"run\", \"python\"]' \"$LARVATAGGER_IMAGE\" /bin/confusion.py" +echo "The confusion.py script is shipped only in images built with argument --target confusion" +echo $DOCKER_RUN +eval $DOCKER_RUN ;; --more-help) @@ -324,16 +346,37 @@ $docker pull ${DOCKER_ARGS}flaur/larvatagger:latest *) cat << EOT +Wrapper script to operate larvatagger.jl in a Docker image. + Usage: $0 build [--stable] [--with-default-backend] [--with-backend <backend>] $0 open <filepath> [...] $0 import <filepath> [<outputfilename>] [...] $0 train <datarepository> <taggername> [--backend <name>] [...] $0 predict <datafile> [--backend <name>] [--model-instance <taggername>] [...] + $0 confusion <datarepository> $0 merge <filepath> [<outputfilename>] [...] + $0 reverse-mapping <filepath> <filename> <outputfilename> $0 --more-help $0 --version $0 --update -See --more-help for more information about additional options [...] to larvatagger.jl + +Note the arguments are very similar to those of larvatagger.jl, except with the train +command. Indeed, the backend is MaggotUBA-adapter per default, if included in the image. + +The build, confusion and reverse-mapping commands are specific to the present script and +do not interface with larvatagger.jl. + +The confusion command crawls the data repository (first argument) in search for +groundtruth.label and predicted.label files, and generates a confusion.csv file wherever +both label files are found. + +The reverse-mapping command takes two sibling label files, the first one with mapped labels, +the second one with unmapped labels. It generates a third label file with demapped labels +from the first file. This is useful when the first file diverges from the second one by some +manual editions, on top of label mapping. + +See --more-help for more information about additional arguments for the other commands from +larvatagger.jl. EOT ;; diff --git a/src/Taggers.jl b/src/Taggers.jl index 38f57c2e1683e57a49b83f3da2c3bf66744e678c..f75bb50625b14544dbda737e13752865f20c4d0d 100644 --- a/src/Taggers.jl +++ b/src/Taggers.jl @@ -2,7 +2,7 @@ module Taggers import PlanarLarvae.Formats, PlanarLarvae.Dataloaders -export Tagger, isbackend, resetmodel, resetdata, train, predict, finetune +export Tagger, isbackend, resetmodel, resetdata, train, predict, finetune, embed struct Tagger backend_dir::String @@ -247,4 +247,14 @@ function finetune(tagger::Tagger; original_instance=nothing, kwargs...) return ret end +function embed(tagger::Tagger; kwargs...) + args = ["--model-instance", tagger.model_instance] + if !isnothing(tagger.sandbox) + push!(args, "--sandbox") + push!(args, tagger.sandbox) + end + parsekwargs!(args, kwargs) + run(Cmd(`poetry run tagging-backend embed $(args)`; dir=tagger.backend_dir)) +end + end # module diff --git a/src/cli.jl b/src/cli.jl index ab9a0341feff42dd394f059febff75c9314090e4..c0a25fbc05090bc4e0ee55eb5c23388c8b7e62d6 100644 --- a/src/cli.jl +++ b/src/cli.jl @@ -9,39 +9,47 @@ using .Toolkit usage = """Larva Tagger. Usage: - larvatagger.jl open <file-path> [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--manual-label=<label>] - larvatagger.jl import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>] [--pixelsize=<μm>] [--overrides=<comma-separated-list>] [--default-label=<label>] [--manual-label=<label>] [--decode] - larvatagger.jl train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>] [--seed=<seed>] - larvatagger.jl predict <backend-path> <model-instance> <data-path> [--output=<filename>] [--make-dataset] [--skip-make-dataset] [--data-isolation] + larvatagger.jl open <file-path> [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--view-factor=<real>] [--manual-label=<label>] + larvatagger.jl import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>] [--pixelsize=<μm>] [--overrides=<comma-separated-list>] [--default-label=<label>] [--manual-label=<label>] [--decode] [--copy-labels] + larvatagger.jl train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>] [--seed=<seed>] [--debug] + larvatagger.jl train <backend-path> <data-path> <model-instance> --fine-tune=<instance> [--balancing-strategy=<strategy>] [--manual-label=<label>] [--iterations=<N>] [--seed=<seed>] [--debug] + larvatagger.jl predict <backend-path> <model-instance> <data-path> [--output=<filename>] [--make-dataset] [--skip-make-dataset] [--data-isolation] [--debug] + larvatagger.jl predict <backend-path> <model-instance> <data-path> --embeddings [--data-isolation] [--debug] larvatagger.jl merge <input-path> <input-file> [<output-file>] [--manual-label=<label>] [--decode] larvatagger.jl -V | --version larvatagger.jl -h | --help Options: - -h --help Show this screen. - -V --version Show version. - -q --quiet Do not show instructions. - --id=<id> Run or assay ID, e.g. `date_time`. - --framerate=<fps> Camera frame rate, in frames per second. - --pixelsize=<μm> Camera pixel size, in micrometers. - --backends=<path> Path to backend repository. - --port=<number> Port number the server listens to. - --viewer Disable editing capabilities. - --browser Automatically open a browser tab at the served location. - --make-dataset Perform the make_dataset stage prior to proceeding to predict_model. - --skip-make-dataset Skip the make_dataset stage (default behavior since TaggingBackends==0.10). - --data-isolation Isolate the backend data directories for parallel tagging of multiple data files. - --sample-size=<N> Sample only N track segments from the data repository. - --layers=<N> (MaggotUBA) Number of layers of the classifier. - --iterations=<N> (MaggotUBA) Number of training iterations (can be two integers separated by a comma). - --seed=<seed> Seed for the backend's random number generators. - --decode Do not encode the labels into integer indices. + -h --help Show this screen. + -V --version Show version. + -q --quiet Do not show instructions. + --id=<id> Run or assay ID, e.g. `date_time`. + --framerate=<fps> Camera frame rate, in frames per second. + --pixelsize=<μm> Camera pixel size, in micrometers. + --backends=<path> Path to backend repository. + --port=<number> Port number the server listens to. + --viewer Disable editing capabilities. + --browser Automatically open a browser tab at the served location. + --view-factor=<real> Scaling factor for the larva views; default is 2 on macOS, 1 elsewhere. + --make-dataset Perform the make_dataset stage prior to proceeding to predict_model. + --skip-make-dataset Skip the make_dataset stage (default behavior since TaggingBackends==0.10). + --data-isolation Isolate the backend data directories for parallel tagging of multiple data files. + --sample-size=<N> Sample only N track segments from the data repository. + --layers=<N> (MaggotUBA) Number of layers of the classifier. + --iterations=<N> (MaggotUBA) Number of training iterations (can be two integers separated by a comma). + --seed=<seed> Seed for the backend's random number generators. + --segment=<t0,t1> Start and end times (included, comma-separated) for cropping and including tracks. + --debug Lower the logging level to DEBUG. + --embeddings (MaggotUBA) Call the backend to generate embeddings instead of labels. + --decode Do not encode the labels into integer indices. + --copy-labels Replicate discrete behavior data from the input file. --default-label=<label> Label all untagged data as <label>. --manual-label=<label> Secondary label for manually labelled data [default: edited]. --labels=<comma-separated-list> Comma-separated list of behavior tags/labels. --class-weights=<csv> Comma-separated list of floats. --pretrained-model=<instance> Name of the pretrained encoder (from `pretrained_models` registry). --balancing-strategy=<strategy> Any of `auto`, `maggotuba`, `none` [default: auto]. + --fine-tune=<instance> Load and fine-tune an already trained model. --overrides=<comma-separated-list> Comma-separated list of key:value pairs. -o <filename> --output=<filename> Predicted labels filename. diff --git a/src/cli_open.jl b/src/cli_open.jl index 023b432a6a9c20d9bfb4657c44b6cabcf268ee97..d62d3740648b993fe875adc55966d5bc49f32709 100644 --- a/src/cli_open.jl +++ b/src/cli_open.jl @@ -9,7 +9,7 @@ export main usage = """LarvaTagger.jl - launch the server-based GUI. Usage: - larvatagger-gui.jl <file-path> [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--manual-label=<label>] + larvatagger-gui.jl <file-path> [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--view-factor=<real>] [--manual-label=<label>] larvatagger-gui.jl -h | --help Options: @@ -19,6 +19,7 @@ Options: --port=<number> Port number the server listens to. --viewer Disable editing capabilities. --browser Automatically open a browser tab at the served location. + --view-factor=<real> Scaling factor for the larva views; default is 2 on macOS, 1 elsewhere. --manual-label=<label> Secondary label for manually labelled data [default: edited]. Backends defined in LarvaTagger project root directory are automatically found. Other @@ -50,10 +51,18 @@ function main(args=ARGS; exit_on_error=false) @error "File not found; did you specify a file path?" infile exit() end + + kwargs = Dict{Symbol, Any}() + viewfactor = parsed_args["--view-factor"] + if !isnothing(viewfactor) + kwargs[:viewfactor] = parse(Float64, viewfactor) + elseif Sys.isapple() + kwargs[:viewfactor] = 2 + end + if parsed_args["--viewer"] - app = larvaviewer(infile) + app = larvaviewer(infile; kwargs...) else - kwargs = Dict{Symbol, Any}() backends = parsed_args["--backends"] if !isnothing(backends) kwargs[:backend_directory] = backends diff --git a/src/cli_toolkit.jl b/src/cli_toolkit.jl index 82a00be34159ba3519c59d0131a40f663ff5c5b0..5614735f43da6693ad420b0a416c32c74792c9b3 100644 --- a/src/cli_toolkit.jl +++ b/src/cli_toolkit.jl @@ -16,9 +16,10 @@ usage = """Larva Tagger. Usage: larvatagger-toolkit.jl import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>] [--pixelsize=<μm>] [--overrides=<comma-separated-list>] [--default-label=<label>] [--manual-label=<label>] [--decode] [--copy-labels] [--segment=<t0,t1>] - larvatagger-toolkit.jl train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>] [--seed=<seed>] - larvatagger-toolkit.jl train <backend-path> <data-path> <model-instance> --fine-tune=<instance> [--balancing-strategy=<strategy>] [--manual-label=<label>] [--iterations=<N>] [--seed=<seed>] - larvatagger-toolkit.jl predict <backend-path> <model-instance> <data-path> [--output=<filename>] [--make-dataset] [--skip-make-dataset] [--data-isolation] + larvatagger-toolkit.jl train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>] [--seed=<seed>] [--debug] + larvatagger-toolkit.jl train <backend-path> <data-path> <model-instance> --fine-tune=<instance> [--balancing-strategy=<strategy>] [--manual-label=<label>] [--iterations=<N>] [--seed=<seed>] [--debug] + larvatagger-toolkit.jl predict <backend-path> <model-instance> <data-path> [--output=<filename>] [--make-dataset] [--skip-make-dataset] [--data-isolation] [--debug] + larvatagger-toolkit.jl predict <backend-path> <model-instance> <data-path> --embeddings [--data-isolation] [--debug] larvatagger-toolkit.jl merge <input-path> <input-file> [<output-file>] [--manual-label=<label>] [--decode] larvatagger-toolkit.jl -V | --version larvatagger-toolkit.jl -h | --help @@ -37,6 +38,8 @@ Options: --iterations=<N> Number of training iterations (integer or comma-separated list of integers). --seed=<seed> Seed for the backend's random number generators. --segment=<t0,t1> Start and end times (included, comma-separated) for cropping and including tracks. + --debug Lower the logging level to DEBUG. + --embeddings (MaggotUBA) Call the backend to generate embeddings instead of labels. --decode Do not encode the labels into integer indices. --copy-labels Replicate discrete behavior data from the input file. --default-label=<label> Label all untagged data as <label>. @@ -161,6 +164,8 @@ function main(args=ARGS; exit_on_error=true) isnothing(iterations) || (kwargs[:iterations] = iterations) seed = parsed_args["--seed"] isnothing(seed) || (kwargs[:seed] = seed) + debug = parsed_args["--debug"] + isnothing(debug) || (kwargs[:debug] = debug) # finetune_model = parsed_args["--fine-tune"] if isnothing(finetune_model) # standard train @@ -185,6 +190,7 @@ function main(args=ARGS; exit_on_error=true) data_path = parsed_args["<data-path>"] data_isolation = parsed_args["--data-isolation"] output_filename = parsed_args["--output"] + embeddings = parsed_args["--embeddings"] # datapath = abspath(data_path) destination = if isfile(datapath) @@ -213,8 +219,13 @@ function main(args=ARGS; exit_on_error=true) end resetdata(tagger) Taggers.push(tagger, datapath) - predict(tagger; skip_make_dataset=parsed_args["--skip-make-dataset"], - make_dataset=parsed_args["--make-dataset"]) + if embeddings + embed(tagger; skip_make_dataset=parsed_args["--skip-make-dataset"], + make_dataset=parsed_args["--make-dataset"], debug=parsed_args["--debug"]) + else + predict(tagger; skip_make_dataset=parsed_args["--skip-make-dataset"], + make_dataset=parsed_args["--make-dataset"], debug=parsed_args["--debug"]) + end Taggers.pull(tagger, destination) end end diff --git a/src/editor.jl b/src/editor.jl index 07e96b2c76233577df9e2c6e260d88d670d0116b..61c33882a40150683a38145aa3c2ab10f7f9e8c2 100644 --- a/src/editor.jl +++ b/src/editor.jl @@ -32,7 +32,8 @@ projectdir = dirname(Base.active_project()) function larvaeditor(path=nothing; allow_multiple_tags::Union{Nothing, Bool}=nothing, backend_directory::AbstractString=projectdir, - manualtag::Union{Nothing, String, Symbol}="edited") + manualtag::Union{Nothing, String, Symbol}="edited", + kwargs...) # to (re-)load a file, the app is reloaded with the filepath as sole information # from previous session @@ -48,7 +49,8 @@ function larvaeditor(path=nothing; editor = EditorView(larvaviewer(controller; editabletags=true, - multipletags=allow_multiple_tags), + multipletags=allow_multiple_tags, + kwargs...), larvafilter(controller), tagfilter(controller; manualtag=manualtag), metadataeditor(controller), diff --git a/src/files.jl b/src/files.jl index bde0c9c763a295ed54564ac2790c1143c96661da..ac616586b19d9fde077cc3b1eb3ae5568ae7933f 100644 --- a/src/files.jl +++ b/src/files.jl @@ -206,6 +206,10 @@ function loadfile(path) if file isa Formats.FIMTrack times = PlanarLarvae.times(data) tracks = [LarvaModel(track, times) for track in values(getrun(file))] + elseif file isa Formats.MaggotUBA + times = PlanarLarvae.times(data) + tracks = Formats.astimeseries(getrun(file); labels2tags=true) + tracks = [LarvaModel(id, ts, times) for (id, ts) in tracks] else times = PlanarLarvae.times(gettimeseries(file)) tracks = [LarvaModel(id, ts, times) for (id, ts) in pairs(gettimeseries(file))] @@ -409,7 +413,7 @@ function explicit_editions_needed(controller, editiontag) if file isa Formats.JSONLabels Formats.load!(file) return haskey(file.run.attributes, :labels) - elseif file isa Formats.Trxmat + elseif file isa Formats.Trxmat || file isa MaggotUBA return true else return false diff --git a/src/models.jl b/src/models.jl index 965fd4e325f7517e0dff96adebba3a0e6c8855fb..2c537415dc328090bbf2133ac7e5b982be09c88c 100644 --- a/src/models.jl +++ b/src/models.jl @@ -109,9 +109,9 @@ end function getusertags(larva, timestep; lut=nothing) usertags = larva.usertags[] tags = UserTags() - try + if haskey(usertags, timestep) tags = usertags[timestep] - catch + else firststep = larva.alignedsteps[1] firststep <= timestep || return tags relstep = timestep - firststep + 1 @@ -250,7 +250,6 @@ function LarvaModel(track::Track, times::Vector{PlanarLarvae.Time}) missingsteps = ones(Bool, steps[end]) missingsteps[steps] .= false missingsteps = findall(missingsteps) - # this method may be dead code; the warning below has never been emitted isempty(missingsteps) || @warn "Time steps are missing" id=convert(Int, track.id) missingsteps path = coordinates.(larvatrack(track[:spine])) usertags = Observable(Dict{TimeStep, UserTags}()) @@ -262,7 +261,28 @@ function LarvaModel(track::Track, times::Vector{PlanarLarvae.Time}) usertags) end -Meshes.boundingbox(larva::LarvaModel) = Meshes.boundingbox([outline(state) for (_,state) in larva.fullstates]) +""" + outline_or_spine(state) + outline_or_spine(pointtype, state) + +Return outline data if available, spine data otherwise. +""" +function outline_or_spine(state) + if haskey(state, :outline) + outline(state) + else + spine(state) + end +end +function outline_or_spine(T, state) + if haskey(state, :outline) + outline(T, state) + else + spine(T, state) + end +end + +Meshes.boundingbox(larva::LarvaModel) = Meshes.boundingbox([outline_or_spine(state) for (_,state) in larva.fullstates]) Meshes.boundingbox(larvae::Vector{LarvaModel}) = Meshes.boundingbox(map(Meshes.boundingbox, larvae)) function downsampler(; step::Int=20) diff --git a/src/players.jl b/src/players.jl index f805cf91b922e61d7bd765e922ab158bfcfc035b..ff1f5fd5ae8ed5fa98a213c2aee584ace3fc1903 100644 --- a/src/players.jl +++ b/src/players.jl @@ -158,6 +158,7 @@ function timecontroller(times::Vector{Float64}; speed=1.0) stepmin = Observable(1) stepmax = Observable(nsteps) boundreached = Observable(true) + initialized = Ref(false) # useful for skipping a "Time bound reached" message at startup # on(timestep) do step min, max = stepmin[], stepmax[] @@ -177,7 +178,8 @@ function timecontroller(times::Vector{Float64}; speed=1.0) end end on(boundreached) do b - b && @info "Time bound reached" + b && initialized[] && @info "Time bound reached" + initialized[] = true end on(stepmin) do bound 0 < bound || throw(DomainError("stepmin < 1")) diff --git a/src/plots.jl b/src/plots.jl index c5b21a867310f223a32dd1829202ae6012cfda38..0364c82020fdb99104df2c9c5f37025810f5b481 100644 --- a/src/plots.jl +++ b/src/plots.jl @@ -13,7 +13,7 @@ end # -const CompatLarvaID = Int16 +const CompatLarvaID = Int32 const ActiveLarva = Union{Nothing, CompatLarvaID} function withalpha(outline_color, alpha_value) @@ -82,7 +82,7 @@ function StatefulLarva(larva::LarvaModel, _, color = gettag(tag_lut, larva, larva.alignedsteps[1], fallback_color) # key observables - shape_outline = Observable(outline(Makie.Point2f, state)) + shape_outline = Observable(outline_or_spine(Makie.Point2f, state)) shape_color = Observable(html_color(color)) visibility = Observable(false) @@ -96,7 +96,7 @@ function StatefulLarva(larva::LarvaModel, end step -= count(larva.missingsteps .< step) _, state = timeseries[step] - shape_outline.val = outline(Makie.Point2f, state) + shape_outline.val = outline_or_spine(Makie.Point2f, state) # _, color = gettag(tag_lut, larva, timestep, fallback_color) shape_color.val = html_color(color) @@ -148,7 +148,7 @@ function SingleLarvaView(larvae::Vector{LarvaModel}, controller; editabletags::B visible = Observable(false) path = Observable(larva.path) pathtree = map(KDTree, path) - shape_outline = Observable(outline(Makie.Point2f, state)) + shape_outline = Observable(outline_or_spine(Makie.Point2f, state)) shape_color = Observable(html_color(color)) # TODO: move the callbacks to `plot!` @@ -201,7 +201,7 @@ function SingleLarvaView(larvae::Vector{LarvaModel}, controller; editabletags::B ) step -= count(larva.missingsteps .< step) _, state = larva.fullstates[step] - shape_outline.val = outline(Makie.Point2f, state) + shape_outline.val = outline_or_spine(Makie.Point2f, state) # _, color = gettag(tag_lut, larva, timestep, fallback_color) shape_color.val = html_color(color) @@ -593,7 +593,7 @@ function DecoratedLarvae(larvae::Vector{DecoratedLarva}) else @error begin j_id = larvae[j].larva.model.id - "cannot decorate invisible larva #$(j_id) - please file an issue at https://gitlab.pasteur.fr/nyx/larvatagger.jl/-/issues" + "Cannot decorate invisible larva #$(j_id)" end hovered_larva.val = 0 end diff --git a/src/viewer.jl b/src/viewer.jl index 6ea1a4c93e0779f2d77fc86d298de3e60cec76c2..7463492dd4eef02ba03de68d9bd57efd5c12df6a 100644 --- a/src/viewer.jl +++ b/src/viewer.jl @@ -42,7 +42,8 @@ function JSServe.jsrender(session::Session, vv::ViewerView) DOM.div(JSServe.TailwindCSS, assaydom, trackdom; class="flex flex-row")) end -function larvaviewer(path::String; allow_multiple_tags::Union{Nothing, Bool}=false) +function larvaviewer(path::String; allow_multiple_tags::Union{Nothing, Bool}=false, + kwargs...) App() do session::Session @@ -50,7 +51,7 @@ function larvaviewer(path::String; allow_multiple_tags::Union{Nothing, Bool}=fal tryopenfile(controller, path) - viewer = larvaviewer(controller; multipletags=allow_multiple_tags) + viewer = larvaviewer(controller; multipletags=allow_multiple_tags, kwargs...) r(session, LarvaTaggerJS) r(session, LarvaTaggerCSS) @@ -62,6 +63,7 @@ end function larvaviewer(controller; editabletags::Bool=false, multipletags::Union{Nothing, Bool}=false, + viewfactor::Real=1, ) model = getlarvae(controller) times = gettimes(controller) @@ -85,10 +87,19 @@ function larvaviewer(controller; @info isnothing(id) ? "No active larva" : "Activating larva #$(id)" end - viewer = ViewerView(assayviewer(delayed_controller), + kwargs = Dict{Symbol, Any}() + if viewfactor != 1 + width, height = FIGSIZE + width = round(Int, width * viewfactor) + height = round(Int, height * viewfactor) + kwargs[:size] = (width, height) + end + + viewer = ViewerView(assayviewer(delayed_controller; kwargs...), trackviewer(controller; editabletags=editabletags, - multipletags=multipletags)) + multipletags=multipletags, + kwargs...)) settimestep!(controller, 1) diff --git a/src/wgl.jl b/src/wgl.jl index c9cad2c70a4623d433ca57ff2afb8b4ac616f8d9..164fe6792251376c606e2a5ef7e82479807772a3 100644 --- a/src/wgl.jl +++ b/src/wgl.jl @@ -118,6 +118,7 @@ struct AssayPlot end function AssayPlot(ctrl, larvae::DecoratedLarvae; size=FIGSIZE) + gethub(ctrl)[:decoratedlarvae] = larvae fig = Figure(resolution=size) width = 0.1f0 # try to get 1px color = RGBAf(0, 0, 0, 0.36) @@ -421,10 +422,10 @@ mutable struct AssayViewer dom end -function assayviewer(controller) +function assayviewer(controller; kwargs...) model = getlarvae(controller) visible = Observable(true) - plot = assayplot(model, controller, visible) + plot = assayplot(model, controller, visible; kwargs...) play = player(controller) on(visible) do b if b @@ -448,14 +449,50 @@ end struct LarvaInfo controller id::LarvaID + hovered::AbstractObservable{Bool} + clicked::AbstractObservable{Bool} reviewed::AbstractObservable{Bool} edited::AbstractObservable{Bool} included::AbstractObservable{Bool} end function larvainfo(controller, id) + hovered = Observable(false) + clicked = Observable(false) reviewed = Observable(false) edited = Observable(false) + # + larvae = gethub(controller)[:decoratedlarvae] + larvaindex = nothing + for (i, larva) in enumerate(larvae.larvae) + larva = larva.larva + if larva.model.id == id + larvaindex = i + end + end + larvavisible = larvae.larvae[larvaindex].larva.visible + on(hovered) do b + if larvae.hovering_active[] + if b + if larvavisible[] + larvae.hovered_larva[] = larvaindex + end + else + i = larvae.hovered_larva[] + if i != 0 + # TODO: additionally condition the below warning on the type of larva + # view (in AssayPlot, warn; in TrackPlot, do not warn) + #i == larvaindex || @warn "Larva #$(larvae.larvae[i].larva.model.id) unexpectedly decorated" + larvae.hovered_larva[] = 0 + end + end + end + end + on(clicked) do b + @assert b + activatelarva!(controller, id) + clicked.val = false + end on(edited) do b if b #@assert reviewed[] @@ -474,6 +511,8 @@ function larvainfo(controller, id) included = Observable(false) LarvaInfo(controller, id, + hovered, + clicked, reviewed, edited, included) @@ -482,7 +521,10 @@ end JSServe.jsrender(session::Session, li::LarvaInfo) = r(session, prerender(li)) function prerender(li::LarvaInfo) - label = "#$(li.id)" + label = DOM.label("#$(li.id)", + onmouseenter=js"JSServe.update_obs($(li.hovered), true)", + onmouseleave=js"JSServe.update_obs($(li.hovered), false)", + onmouseup=js"JSServe.update_obs($(li.clicked), true)") discard_larva_edits = js"LarvaTagger.discardLarvaEdits(this, $(li.edited), $label)" reviewed_checkbox = DOM.input(type="checkbox", checked=li.reviewed, @@ -501,8 +543,7 @@ function prerender(li::LarvaInfo) DOM.tr(DOM.td(label), DOM.td(reviewed_checkbox, style="text-align: center;"), DOM.td(edited_checkbox, style="text-align: center;"), - DOM.td(included_checkbox, style="text-align: center;"), - ) + DOM.td(included_checkbox, style="text-align: center;")) end struct LarvaFilter @@ -990,9 +1031,10 @@ end function trackviewer(controller; editabletags::Bool=true, multipletags::Union{Nothing, Bool}=nothing, + kwargs... ) model = getlarvae(controller) - plot = trackplot(model, controller; editabletags=editabletags) + plot = trackplot(model, controller; editabletags=editabletags, kwargs...) play = player(controller) sele = tagselector(controller; editabletags=editabletags, multipletags=multipletags) TrackViewer(plot, play, sele, nothing) diff --git a/test/deploy_and_test.sh b/test/deploy_and_test.sh index 71e3406660068220d960b8a8175a82e5cb94a681..4d3ca51c7c3f1c59d122a296b6a3df53a4ea53f6 100755 --- a/test/deploy_and_test.sh +++ b/test/deploy_and_test.sh @@ -103,7 +103,7 @@ if [ -f ../../LarvaTagger_test_data.tgz ]; then tar zxvf ../../LarvaTagger_test_data.tgz else # Not recommended; reproducibility is not guarantee across hosts or architectures yet - wget -O- https://dl.pasteur.fr/fop/l3qruSQR/LarvaTagger_test_data.tgz | tar zxv + wget -O- https://dl.pasteur.fr/fop/8MvgygD4/LarvaTagger_test_data.tgz | tar zxv fi if [ "$LOCAL_SCENARII" = "1" ]; then diff --git a/test/scenarii.sh b/test/scenarii.sh index 59a96d49a14ad72fc414f258b986496c9cd975a9..cfe2784ad504372ec4396e644b400137203a0839 100755 --- a/test/scenarii.sh +++ b/test/scenarii.sh @@ -44,6 +44,10 @@ maggotuba="../MaggotUBA" seed=1028347112001 +endTest() { + echo "----------------------------------------------------------------------------" +} + prepareTestData() { tmpdir="$SHUNIT_TMPDIR/$1" rm -rf "$tmpdir" @@ -79,6 +83,7 @@ testImportLabelFile() { assertTrue '\`import\` failed to reproduce the imported.label file' '$(cmp "$datapath/imported.label" "$datapath/$filename")' # clean up rm -f "$datapath/$filename" + endTest } # requires: cropped.label @@ -92,6 +97,7 @@ testCropTracks() { assertTrue '\`import\` failed to reproduce the cropped.label file' '$(cmp "$datapath/cropped.label" "$datapath/$filename")' # clean up rm -f "$datapath/$filename" + endTest } # requires: sample.spine sample.outline test_train_default/predicted.label @@ -106,14 +112,15 @@ testPredictDefault() { [ -f "$maggotuba/models/$tagger/clf_config.json" ] || exit 1 # run cd "$project_root" - echo "\"$larvataggerjl\" predict \"$maggotuba\" $tagger \"$tmpdir/sample.spine\"" - "$larvataggerjl" predict "$maggotuba" $tagger "$tmpdir/sample.spine" + echo "\"$larvataggerjl\" predict \"$maggotuba\" $tagger \"$tmpdir/sample.spine\" --debug" + "$larvataggerjl" predict "$maggotuba" $tagger "$tmpdir/sample.spine" --debug # compare filename=predicted.label predictions="$tmpdir/$filename" expected_labels="$datapath/$tagger/$filename" assertFalse "\`predict\` failed to generate file $filename" '[ -z "$predictions" ]' assertTrue "\`predict\` failed to reproduce file $filename" '$(cmp "$expected_labels" "$predictions")' + endTest } # requires: sample.spine sample.outline original_predictions.label test_train_default/* @@ -122,10 +129,11 @@ testTrainDefault() { tmpdir=$(prepareTrainingData original_predictions.label $tagger) # run cd "$project_root" - echo "\"$larvataggerjl\" train \"$maggotuba\" \"$tmpdir\" $tagger --seed $seed" - "$larvataggerjl" train "$maggotuba" "$tmpdir" $tagger --seed $seed + echo "\"$larvataggerjl\" train \"$maggotuba\" \"$tmpdir\" $tagger --seed $seed --debug" + "$larvataggerjl" train "$maggotuba" "$tmpdir" $tagger --seed $seed --debug # test postTrain $tagger + endTest } # requires: sample.spine sample.outline imported.label test_train_one_class/* @@ -138,6 +146,7 @@ testTrainOneClass() { "$larvataggerjl" train "$maggotuba" "$tmpdir" $tagger --iterations 10 --seed $seed --labels="back-up,not back-up" # test postTrain $tagger + endTest } # requires: sample.spine sample.outline imported.label test_train_one_class_with_weights/* @@ -150,6 +159,7 @@ testTrainOneClassWithWeights() { "$larvataggerjl" train "$maggotuba" "$tmpdir" $tagger --iterations 10 --seed $seed --labels="not back-up,back-up" --class-weights 1,10 # test postTrain $tagger + endTest } # requires: sample.spine sample.outline gui_imported.label test_train_one_class_with_encoder/* @@ -162,6 +172,7 @@ testTrainOneClassWithEncoder() { "$larvataggerjl" train "$maggotuba" "$tmpdir" $tagger --iterations 10 --seed $seed --labels="hunch,¬hunch" --pretrained-model=20230524-hunch-25 --balancing-strategy=maggotuba # test postTrain $tagger + endTest } # requires: sample.spine sample.outline trx.mat gui_imported.label original_predictions.label test_train_selected_files/* @@ -178,6 +189,7 @@ testTrainSelectedFiles() { # test cd "$project_root" postTrain $tagger + endTest } # requires: sample.spine sample.outline trx.mat gui_imported.label original_predictions.label test_train_recursive_selection/* @@ -191,6 +203,7 @@ testTrainRecursiveSelection() { "$larvataggerjl" train "$maggotuba" "$tmpdir/**/gui_imported.label" $tagger --seed $seed --labels="run_large,cast_large,hunch_large" --balancing-strategy=maggotuba --iterations=10 # test postTrain $tagger + endTest } postTrain() {