diff --git a/Project.toml b/Project.toml index a16dcb853f410ae6c20ed6d3512a0966027c2265..5f5beef3ad2f1c6a0912e4c8d93d0527006a0d8f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,2 +1,4 @@ [deps] +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" LazyArtifacts = "4af54fe1-eca0-43a8-85a7-787d91b784e3" +PlanarLarvae = "c2615984-ef14-4d40-b148-916c85b43307" diff --git a/pyproject.toml b/pyproject.toml index 434116f2709ad4c12b6fc2ae0e9851d6650467e6..2f85c89f5168f58b7b8b16838dfda2a9718e03e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "MaggotUBA-adapter" -version = "0.16.1" +version = "0.16.2" description = "Interface between MaggotUBA and the Nyx tagging UI" authors = ["François Laurent"] license = "MIT" diff --git a/scripts/revert_label_mapping.jl b/scripts/revert_label_mapping.jl new file mode 100755 index 0000000000000000000000000000000000000000..6824269fc5dac5d883353ee850dda51a6cff9a20 --- /dev/null +++ b/scripts/revert_label_mapping.jl @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +#= +if realpath --version &> /dev/null; then +macos_realpath=$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd -P) +PROJECT_DIR=$(dirname $macos_realpath) +else +PROJECT_DIR=$(dirname $(dirname $(realpath "${BASH_SOURCE[0]}"))) +fi +if [ -z "$JULIA" ]; then JULIA=julia; fi +exec $JULIA --project="$PROJECT_DIR" --color=yes --startup-file=no "${BASH_SOURCE[0]}" "$@" +=# + +using PlanarLarvae.LarvaBase, PlanarLarvae.Datasets, PlanarLarvae.Formats +using JSON3 + +""" + revert_label_mapping(output_label_file, unmapped_label_file, edited_label_file, clfconfig_file) + +Reverse-map the labels while keeping the manual editions made on top of mapped labels. + +All input arguments are file paths. + +This is useful for example with taggers `20230311-0` and `20230311` (same predictions, with +labels mapped from 12 labels to 7 labels): + +* `edited_label_file` saved after manually editing a file generated by the `20230311` tagger + (mapped labels); +* `unmapped_label_file` generated by `20230311-0` (original labels prior to label mapping and + editing); can be a *trx.mat* file instead; +* `clfconfig_file`: *clf_config.json* file for tagger `20230311` (specifies label mapping); +* `output_label_file`: resulting json *.label* file. +""" +function revert_label_mapping(output_label_file, unmapped_label_file, edited_label_file, clfconfig_file) + clfconfig = JSON3.read(clfconfig_file) + legal_labels = collect(clfconfig["original_behavior_labels"]) + mapping = zip(legal_labels, clfconfig["behavior_labels"]) + reverse_mapping = mergewith(vcat, (Dict(v=>[k]) for (k, v) in mapping)...) + original_needed = Dict{String, Vector{String}}() + for (k, v) in pairs(reverse_mapping) + if 1 < length(v) + original_needed[k] = pop!(reverse_mapping, k) + end + end + reverse_mapping = Dict(k=>v[1] for (k, v) in pairs(reverse_mapping)) + # + input_labels = load(edited_label_file) + original_labels = preload(unmapped_label_file) + if !isempty(original_needed) + if original_labels isa Formats.Trxmat + Formats.drop_spines!(original_labels) + Formats.drop_outlines!(original_labels) + end + original_labels = Formats.getnativerepr(original_labels) + if original_labels isa Run + decodelabels!(original_labels) + end + end + run = decodelabels!(getrun(input_labels)) + # + for track in values(run) + labels = track[:labels] + for (i, labels′) in enumerate(labels) + label = if labels′ isa Vector + @assert length(labels′) == 2 && labels′[2] == "edited" + labels′[1] + else + @assert labels′ isa AbstractString + labels′ + end + if label in keys(reverse_mapping) + label = reverse_mapping[label] + else + original = if original_labels isa Run + labels″ = original_labels[track.id][:labels][i] + labels″ isa Vector ? labels″ : [labels″] + elseif original_labels isa LarvaBase.Larvae + _, labels″ = original_labels[track.id][i] + convert(Vector{String}, labels″[:tags]) + end + for label′ in original + if label′ in legal_labels + if label′ ∉ original_needed[label] + @warn "Restored label qualitatively differs" label label′ track.id step=i + end + label = label′ + break + end + end + end + @assert label in legal_labels + if labels′ isa Vector + labels′[1] = label + else + labels′ = label + end + labels[i] = labels′ + end + end + # + if run.attributes[:labels] isa AbstractDict + run.attributes[:labels][:names] = legal_labels + else + run.attributes[:labels] = legal_labels + end + secondary_labels = Datasets.getsecondarylabels(run) + if !isnothing(secondary_labels) + foreach(secondary_labels) do label + push!(legal_labels, label) + end + end + encodelabels!(run; labels=legal_labels, storelabels=false) + # + if :metadata in keys(run.attributes) + metadata = run.attributes[:metadata] + pop!(metadata, :software, nothing) + if isempty(metadata) + pop!(run.attributes, :metadata) + end + end + # + Datasets.to_json_file(output_label_file, run) +end + +usage = """revert_label_mapping.jl + +Usage: + revert_label_mapping.jl <output-file> <unmapped-labels> <edited-labels> <clfconfig-file> + +Options: + -h, --help Show this message. + +Arguments: + <output-file> Output .label file path with reverse-mapped labels and manual editions. + <unmapped-labels> Unmapped .label file path (e.g. from tagger 20230311-0). + <edited-labels> .label file path with mapped labels (e.g. from tagger 20230311) and + manually edited afterwards. + <clfconfig-file> Tagger's clf_config.json file path with label mapping specification + (e.g. 20230311's). +""" + +function main(args=ARGS) + if args isa AbstractString + args = split(args) + end + if length(args) != 4 || "-h" in args || "--help" in args + print(usage) + else + revert_label_mapping(args...) + end +end + +if abspath(PROGRAM_FILE) == @__FILE__ + main() +end