import json import os import streamlit as st from PIL import Image, ImageDraw try: from streamlit_image_coordinates import streamlit_image_coordinates except Exception: streamlit_image_coordinates = None st.set_page_config(page_title="Point Annotation Tool", layout="wide") st.title("Point Annotation Tool") image_dir = st.sidebar.text_input("Image folder", value=".") output_json = st.sidebar.text_input("Output JSON", value="annotations.json") if "index" not in st.session_state: st.session_state.index = 0 if "annotations" not in st.session_state: st.session_state.annotations = {} def list_images(folder): if not os.path.isdir(folder): return [] exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} return sorted([name for name in os.listdir(folder) if os.path.splitext(name.lower())[1] in exts]) def annotated_image(image, points): preview = image.copy() draw = ImageDraw.Draw(preview) for x, y in points: r = 5 draw.ellipse((x - r, y - r, x + r, y + r), fill="red", outline="white") return preview images = list_images(image_dir) if not images: st.warning("No images found.") st.stop() st.session_state.index = max(0, min(st.session_state.index, len(images) - 1)) image_name = images[st.session_state.index] image_path = os.path.join(image_dir, image_name) image = Image.open(image_path).convert("RGB") points = st.session_state.annotations.setdefault(image_name, []) nav1, nav2, nav3, nav4 = st.columns(4) if nav1.button("Prev"): st.session_state.index = max(0, st.session_state.index - 1) st.rerun() if nav2.button("Next"): st.session_state.index = min(len(images) - 1, st.session_state.index + 1) st.rerun() if nav3.button("Delete Last") and points: points.pop() st.rerun() if nav4.button("Clear"): st.session_state.annotations[image_name] = [] st.rerun() st.write(f"{st.session_state.index + 1}/{len(images)} | {image_name} | Count: {len(points)}") preview = annotated_image(image, points) if streamlit_image_coordinates is not None: clicked = streamlit_image_coordinates(preview, key=f"img_{image_name}_{len(points)}") if clicked is not None and "x" in clicked and "y" in clicked: points.append([int(clicked["x"]), int(clicked["y"])]) st.rerun() else: st.image(preview, caption="Install streamlit-image-coordinates for direct click annotation.", use_container_width=True) c1, c2, c3 = st.columns(3) x = c1.number_input("x", min_value=0, max_value=image.width, value=0, step=1) y = c2.number_input("y", min_value=0, max_value=image.height, value=0, step=1) if c3.button("Add Point"): points.append([int(x), int(y)]) st.rerun() export_data = [ {"image": name, "points": pts, "count": len(pts)} for name, pts in sorted(st.session_state.annotations.items()) ] json_text = json.dumps(export_data, indent=2) if st.sidebar.button("Save JSON"): with open(output_json, "w", encoding="utf-8") as f: f.write(json_text) st.sidebar.success(output_json) st.sidebar.download_button( "Download JSON", data=json_text.encode("utf-8"), file_name=os.path.basename(output_json), mime="application/json", )