I went back to a simpler approach: everything now goes through st.session_state
, with direct writing to the JSON (to create an annotation) and edit/save buttons used when needed.
I have a function that writes directly to the JSON file when it’s called — that is, when the user clicks on the photo.
def sauvegarder_annotations(annotations_files):
try:
with open(annotations_file, "w", encoding="utf-8") as f:
json.dump(st.session_state.annotations, f, ensure_ascii=False, indent=2)
except Exception as e:
st.error(f"❌ Erreur lors de la sauvegarde : {e}")
coords = streamlit_image_coordinates(merged_img, key="click_key", width=800)
# Sauvegarde annotation
if coords:
x_orig = int(coords["x"] / ratio)
y_orig = int(coords["y"] / ratio)
angle_ajuste = calculer_angle_porte(x_orig, full_width, direction, fov=fov_user)
if (x_orig, y_orig) not in [(a["x"], a["y"]) for a in st.session_state.annotations[image_name]]:
st.session_state.annotations[image_name].append({
"uuid": str(uuid.uuid4()), # Génère un UUID unique
"x": x_orig, "y": y_orig,
"label": label,
"type_utilisation": type_utilisation,
"cible": cible,
"angle_ajuste": angle_ajuste
})
# Sauvegarder dans le fichier JSON
sauvegarder_annotations(st.session_state.annotations)
st.rerun()
I also have a selectbox that allows the user to choose an annotation to modify or delete, along with associated buttons to confirm the action and save it to the JSON (using the same function as the one triggered by the click).
with col2:
st.markdown("### 📌 Coordonnées enregistrées")
if not df_annotations.empty:
st.dataframe(df_annotations, use_container_width=True, hide_index=True)
# Liste des index avec None pour "aucune sélection"
index_options = [None] + list(df_annotations['index'])
# Affichage formaté : "1 - label - type_utilisation"
def format_option(idx):
if idx is None:
return "Sélectionnez une annotation"
row = df_annotations[df_annotations['index'] == idx].iloc[0]
return f"{idx} - {row['label']} - {row['type_utilisation']}"
selected_index = st.selectbox(
"Sélectionner une annotation",
options=index_options,
format_func=lambda x: "Sélectionnez une annotation" if x is None else f"{x} - {df_annotations[df_annotations['index'] == x].iloc[0]['label']}",
key="selectbox_index"
)
# Mise à jour de selected_annotation uniquement si besoin
if selected_index is not None:
selected_uuid = df_annotations[df_annotations['index'] == selected_index].iloc[0]['uuid']
if st.session_state.get("selected_annotation") != selected_uuid:
st.session_state.selected_annotation = selected_uuid
st.rerun()
else:
if st.session_state.get("selected_annotation") is not None:
st.session_state.selected_annotation = None
st.rerun()
# Utilise toujours l'UUID pour retrouver l'annotation à modifier/supprimer
if st.session_state.selected_annotation is not None:
ann_to_edit = next((ann for ann in st.session_state.annotations[image_name]
if ann["uuid"] == st.session_state.selected_annotation), None)
if ann_to_edit:
st.markdown("### Modifier l'annotation")
cibles = list({c for details in config["annotations"].values() for c in details["cible"]})
cible_edit = st.selectbox("Cible", cibles,
index=cibles.index(ann_to_edit["cible"]),
key="cible_selectbox_edit")
filtered_labels = [label for label, details in config["annotations"].items()
if cible_edit in details["cible"]]
label_edit = st.selectbox("Label", filtered_labels,
index=filtered_labels.index(ann_to_edit["label"]),
key="label_selectbox_edit")
type_utilisation_options = config["annotations"][label_edit]["type_utilisation"]
type_utilisation_edit = st.selectbox("Type d'utilisation",
type_utilisation_options,
index=type_utilisation_options.index(ann_to_edit["type_utilisation"]),
key="type_utilisation_edit")
col1, col2 = st.columns(2)
with col1:
if st.button("📝 Modifier", key=f"modify_{st.session_state.selected_annotation}"):
# Modification dans le fichier
ann_to_edit.update({
"cible": cible_edit,
"label": label_edit,
"type_utilisation": type_utilisation_edit
})
sauvegarder_annotations(st.session_state.annotations)
st.success("Modifications appliquées")
st.rerun()
with col2:
if st.button("🗑️ Supprimer", key=f"delete_{st.session_state.selected_annotation}"):
# Trouve l'index de l'annotation à supprimer
index_to_delete = next(
(i for i, ann in enumerate(st.session_state.annotations[image_name])
if ann["uuid"] == st.session_state.selected_annotation),
None
)
# Supprime directement via del si trouvé
if index_to_delete is not None:
del st.session_state.annotations[image_name][index_to_delete]
sauvegarder_annotations(st.session_state.annotations)
st.success("Annotation supprimée")
st.rerun()
else:
st.markdown("---")
Modification works fine, even for the most recently added annotation, but deletion doesn’t. If I refresh the page (F5), I can delete the annotation.
This block, which resets all annotations for an image, works regardless of when the annotation was made, even though the underlying logic is different.
if st.button("🗑️ Réinitialiser l'image actuelle") and image_files:
st.session_state.annotations[image_files[st.session_state.current_image_index]] = []
sauvegarder_annotations(st.session_state.annotations)
st.rerun()
I tried resetting streamlit_image_coordinates
in case it was retaining the last click and reinjecting it during a rerun — but I removed that cleanup since it didn’t change anything (or maybe I wasn’t doing it correctly).
However, if I delete the last annotation, it rolls back. I can see in my JSON that the annotation disappears — and instantly comes back. It even gets assigned a new UUID at the moment of the rollback, and retains the value it had if I edited the annotation before deleting it.
If I add a new point, there’s no rollback on the previously “new annotation”, but the newly added point inherits the issue.
I tried many things, like having dedicated functions for deletion, etc., but nothing worked. So I wanted to start again with something simple before making the logic more complex.
I hope there’s enough code/context here to understand the issue, as it’s a bit complicated to share the full implementation for now.
Voici une traduction en anglais claire et fidèle de ton message :
Update and clarification observed today:
I noticed that after a rollback, the annotation’s value takes the current value from the creation selectboxes.
For example: I enter a “front door”, then I modify it to a “garage door”.
I change the values in the creation selectboxes to “wooden window” (which are independent from the modification selectboxes).
Now, if I delete the last added annotation, the rollback restores the value from the creation selectboxes, not the original annotation value as I expected — so my “front door” gets recreated, but the value is now “wooden window”.
Which might confirm that the last created annotation remains cached and comes back when the code is re-run (but only upon deletion).