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