88 lines
3.5 KiB
Python
88 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .json_io import read_json
|
|
from .feedback_boxes import valid_feedback_box
|
|
|
|
Log = Callable[[str], None]
|
|
|
|
|
|
def apply_checkbox_actions(
|
|
labels_data: dict[str, dict[str, Any]],
|
|
actions: list[dict[str, Any]],
|
|
log: Log,
|
|
) -> set[str]:
|
|
actions_by_label: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
for action in actions:
|
|
actions_by_label[str(action.get("label", ""))].append(action)
|
|
|
|
dirty_labels: set[str] = set()
|
|
for label, label_actions in actions_by_label.items():
|
|
if label not in labels_data:
|
|
continue
|
|
result = labels_data[label]["result"]
|
|
feedbacks = result.get("feedback", [])
|
|
# Match the renderer's fallback for invalid boxes so checkbox indices
|
|
# still address the right comment when returned annotations are read.
|
|
global_feedbacks = [item for item in feedbacks if not valid_feedback_box(item.get("box_2d"))]
|
|
local_feedbacks = [item for item in feedbacks if valid_feedback_box(item.get("box_2d"))]
|
|
local_feedbacks.sort(key=lambda item: item["box_2d"][0])
|
|
|
|
for action in label_actions:
|
|
action_type = action.get("type")
|
|
if action_type == "score":
|
|
result["score"] = action.get("value")
|
|
dirty_labels.add(label)
|
|
log(f" > Updated score for {label} to {action.get('value')}")
|
|
elif action_type == "clear_all":
|
|
for feedback in feedbacks:
|
|
feedback["to_delete"] = True
|
|
if feedback.get("box_2d"):
|
|
feedback["norectangle"] = True
|
|
dirty_labels.add(label)
|
|
log(f" > Cleared all feedbacks in {label}")
|
|
elif action_type == "del_global":
|
|
index = int(action.get("index", -1))
|
|
if 0 <= index < len(global_feedbacks):
|
|
global_feedbacks[index]["to_delete"] = True
|
|
dirty_labels.add(label)
|
|
log(f" > Deleted global feedback in {label}")
|
|
elif action_type in {"del_local", "del_local_rect"}:
|
|
index = int(action.get("index", -1))
|
|
if 0 <= index < len(local_feedbacks):
|
|
target = local_feedbacks[index]
|
|
if action_type == "del_local":
|
|
target["to_delete"] = True
|
|
log(f" > Deleted local feedback in {label}")
|
|
else:
|
|
target["norectangle"] = True
|
|
log(f" > Deleted rectangle in {label}")
|
|
dirty_labels.add(label)
|
|
return dirty_labels
|
|
|
|
|
|
def apply_score_overrides(
|
|
labels_data: dict[str, dict[str, Any]],
|
|
score_path: Path,
|
|
log: Log,
|
|
) -> set[str]:
|
|
if not score_path.exists():
|
|
return set()
|
|
loaded = read_json(score_path)
|
|
if not isinstance(loaded, dict):
|
|
raise TypeError(f"Expected a JSON object in {score_path}")
|
|
dirty: set[str] = set()
|
|
for label, score in loaded.items():
|
|
if label not in labels_data:
|
|
continue
|
|
current = str(labels_data[label]["result"].get("score", 0))
|
|
if current != str(score):
|
|
labels_data[label]["result"]["score"] = score
|
|
dirty.add(label)
|
|
log(f" > Overrode score for {label} to {score} from score.json")
|
|
return dirty
|