Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 

310 rader
16 KiB

  1. import os
  2. import json
  3. import streamlit as st
  4. import pandas as pd
  5. from PIL import Image
  6. from pathlib import Path
  7. import time
  8. import folium
  9. from streamlit_folium import st_folium
  10. import math
  11. import base64
  12. from io import BytesIO
  13. # --- CACHE IMMAGINE ---
  14. @st.cache_data
  15. def get_image_base64(img_path):
  16. img = Image.open(img_path).convert("RGBA")
  17. w, h = img.size
  18. buffered = BytesIO()
  19. img.save(buffered, format="PNG")
  20. img_str = base64.b64encode(buffered.getvalue()).decode("ascii")
  21. return f"data:image/png;base64,{img_str}", w, h
  22. # --- FUNZIONE SALVATAGGIO FISICO ---
  23. def force_save_json(path, data):
  24. try:
  25. with open(path, "w", encoding='utf-8') as f:
  26. json.dump(data, f, indent=4)
  27. return True
  28. except Exception as e:
  29. st.error(f"Errore scrittura disco: {e}")
  30. return False
  31. def show_mapper(cfg):
  32. # Configurazione percorsi
  33. MAPS_DIR = Path(cfg['maps']['map_dir'])
  34. TRAIN_BASE = Path("/data/train")
  35. JOBS_BASE = Path(cfg['collect_train']['jobs_dir'])
  36. # Riferimenti directory per Addestramento e Test
  37. SAMPLES_DIR = TRAIN_BASE / "samples"
  38. TEST_SAMPLES_DIR = TRAIN_BASE / "testsamples"
  39. PENDING_DIR = JOBS_BASE / "pending"
  40. ERROR_DIR = JOBS_BASE / "error"
  41. TEST_PENDING_DIR = TRAIN_BASE / "testjobs/pending"
  42. TEST_ERROR_DIR = TRAIN_BASE / "testjobs/error"
  43. # File configurazione
  44. BEACONS_FILE = cfg.get('paths', {}).get('beacons_csv', "/data/config/beacons.csv")
  45. GROUPS_FILE = "/data/config/beacongroup.csv"
  46. CSV_DELIM = cfg.get('paths', {}).get('csv_delimiter', ';')
  47. # Creazione directory se mancano
  48. for p in [MAPS_DIR, SAMPLES_DIR, TEST_SAMPLES_DIR, PENDING_DIR, ERROR_DIR, TEST_PENDING_DIR, TEST_ERROR_DIR]:
  49. p.mkdir(parents=True, exist_ok=True)
  50. # --- 1. GESTIONE UPLOAD ---
  51. maps = sorted([f.replace(cfg['maps']['floor_prefix'], "").split('.')[0]
  52. for f in os.listdir(MAPS_DIR) if f.startswith(cfg['maps']['floor_prefix'])])
  53. with st.expander("📂 Carica Nuova Planimetria", expanded=not maps):
  54. c_up1, c_up2 = st.columns([1, 2])
  55. new_f_id = c_up1.text_input("ID Piano (es. 0, -1):", key="new_f_id_v21")
  56. up_file = c_up2.file_uploader("Immagine (PNG/JPG):", type=['png', 'jpg', 'jpeg'], key="up_v21")
  57. if st.button("🚀 SALVA PIANO", use_container_width=True):
  58. if new_f_id and up_file:
  59. ext = Path(up_file.name).suffix
  60. img_save_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{new_f_id}{ext}"
  61. with open(img_save_path, "wb") as f:
  62. f.write(up_file.getbuffer())
  63. m_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{new_f_id}.json"
  64. force_save_json(m_path, {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0], "grid_size": 50})
  65. st.session_state.current_floor = new_f_id
  66. st.rerun()
  67. if not maps: return
  68. # --- 2. RIGA STATO ---
  69. c_p, c_s, c_o = st.columns([2.5, 2, 2])
  70. with c_p:
  71. s1, s2 = st.columns([1, 1.2])
  72. s1.markdown("<p style='padding-top:35px; font-weight:bold;'>Piano Attivo:</p>", unsafe_allow_html=True)
  73. def_idx = maps.index(st.session_state.current_floor) if st.session_state.get("current_floor") in maps else 0
  74. floor_id = s2.selectbox("", maps, index=def_idx, key="f_v21", label_visibility="collapsed")
  75. if st.session_state.get("prev_floor") != floor_id:
  76. st.session_state.cal_points = []
  77. st.session_state.temp_lat, st.session_state.temp_lng = None, None
  78. st.session_state.prev_floor = floor_id
  79. meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json"
  80. if meta_path.exists():
  81. with open(meta_path, "r") as f: meta = json.load(f)
  82. else:
  83. meta = {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0]}
  84. with c_s: st.info(f"📏 Scala: {'✅' if meta['calibrated'] else '❌'}\n({meta['pixel_ratio']:.4f} px/cm)")
  85. with c_o: st.info(f"🎯 Origine: {'✅' if meta['origin'] != [0,0] else '❌'}\n(X:{meta['origin'][0]}, Y:{meta['origin'][1]})")
  86. # --- 3. IMPOSTAZIONI GLOBALI ---
  87. st.markdown("---")
  88. g1, g2, g3, g4 = st.columns([1.2, 1.5, 1.2, 2])
  89. with g1: show_grid = st.toggle("Griglia", value=False, key="grid_v21")
  90. with g2: grid_cm = st.select_slider("Passo (cm):", options=[25, 50, 100, 200], value=50, key="step_v21")
  91. with g3: snap_on = st.toggle("Stay Grid", value=False, key="snap_v21")
  92. with g4: m_size = st.slider("Dimensione Marker:", 5, 20, 8, key="msize_v21")
  93. st.markdown("---")
  94. # --- 4. TOOLSET ---
  95. t1, t2, t3 = st.columns(3)
  96. if t1.button("📏 CALIBRA", use_container_width=True): st.session_state.map_tool = "Calibra"; st.session_state.cal_points = []
  97. if t2.button("🎯 SET ORIGINE", use_container_width=True): st.session_state.map_tool = "Origine"
  98. if t3.button("📡 RILEVA", use_container_width=True): st.session_state.map_tool = "Rileva"
  99. tool = st.session_state.get('map_tool', 'Rileva')
  100. # --- 5. MAPPA ---
  101. col_map, col_ui = st.columns([3, 1.2])
  102. with col_map:
  103. img_p = next((MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}" for e in ['.png','.jpg','.jpeg'] if (MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}").exists()))
  104. img_data, w, h = get_image_base64(img_p)
  105. bounds = [[0, 0], [h, w]]
  106. m = folium.Map(location=[h/2, w/2], crs="Simple", tiles=None, attribution_control=False)
  107. m.fit_bounds(bounds)
  108. m.options.update({"minZoom": -6, "maxZoom": 6, "zoomSnap": 0.25, "maxBounds": bounds, "maxBoundsViscosity": 1.0})
  109. folium.raster_layers.ImageOverlay(image=img_data, bounds=bounds, interactive=True).add_to(m)
  110. if meta["origin"] != [0, 0]:
  111. ox, oy = meta["origin"]
  112. folium.CircleMarker(location=[oy, ox], radius=6, color="black", fill=True, zindex=1000).add_to(m)
  113. if show_grid and meta["calibrated"] and meta["origin"] != [0, 0]:
  114. px_step = grid_cm * meta["pixel_ratio"]
  115. ox, oy = meta["origin"]
  116. for x in sorted(list(range(int(ox), w, int(px_step))) + list(range(int(ox), 0, -int(px_step)))):
  117. folium.PolyLine([[0, x], [h, x]], color="blue", weight=1, opacity=0.1).add_to(m)
  118. for y in sorted(list(range(int(oy), h, int(px_step))) + list(range(int(oy), 0, -int(px_step)))):
  119. folium.PolyLine([[y, 0], [y, w]], color="blue", weight=1, opacity=0.1).add_to(m)
  120. # --- DISEGNO PUNTI STORICI ---
  121. def draw_historical_samples(directory, color, shape="circle", is_test=False):
  122. if not meta["calibrated"] or meta["origin"] == [0,0]: return
  123. for f in Path(directory).glob("*.csv"):
  124. try:
  125. parts = f.stem.split('_')
  126. if len(parts) < 4: continue
  127. fz = parts[-3]
  128. fx = float(parts[-2].replace(',', '.'))
  129. fy = float(parts[-1].replace(',', '.'))
  130. if str(fz) == str(floor_id):
  131. px_x = (fx * meta["pixel_ratio"]) + meta["origin"][0]
  132. px_y = meta["origin"][1] - (fy * meta["pixel_ratio"])
  133. dash = "5, 5" if is_test else None
  134. weight = 3 if is_test else 1
  135. folium.CircleMarker(
  136. location=[px_y, px_x], radius=m_size, color=color,
  137. weight=weight, dash_array=dash, fill=True, fill_opacity=0.7
  138. ).add_to(m)
  139. except: continue
  140. draw_historical_samples(SAMPLES_DIR, "green", "circle", is_test=False)
  141. draw_historical_samples(TEST_SAMPLES_DIR, "#00FFFF", "circle", is_test=True)
  142. def draw_jobs(directory, color, shape="diamond", is_test=False):
  143. if not meta["calibrated"] or meta["origin"] == [0,0]: return
  144. for f in Path(directory).glob("*.csv"):
  145. try:
  146. df = pd.read_csv(f, sep=";")
  147. df.columns = [c.lower() for c in df.columns]
  148. row = df.iloc[0]
  149. z_val = str(row.get('z', row.get('floor')))
  150. if z_val == str(floor_id):
  151. px_x = (row['x'] * meta["pixel_ratio"]) + meta["origin"][0]
  152. px_y = meta["origin"][1] - (row['y'] * meta["pixel_ratio"])
  153. dash = "3, 3" if is_test else None
  154. folium.RegularPolygonMarker(
  155. location=[px_y, px_x], number_of_sides=4,
  156. rotation=45 if shape=="diamond" else 0,
  157. radius=m_size, color=color, weight=2,
  158. dash_array=dash, fill=True
  159. ).add_to(m)
  160. except: continue
  161. draw_jobs(PENDING_DIR, "orange", "diamond", False)
  162. draw_jobs(TEST_PENDING_DIR, "#f1c40f", "diamond", True)
  163. draw_jobs(ERROR_DIR, "red", "square", False)
  164. draw_jobs(TEST_ERROR_DIR, "#e74c3c", "square", True)
  165. if tool == "Calibra" and st.session_state.get("cal_points"):
  166. for p in st.session_state.cal_points: folium.CircleMarker(location=p, radius=6, color="red", fill=True).add_to(m)
  167. if len(st.session_state.cal_points) == 2: folium.PolyLine(st.session_state.cal_points, color="red", weight=3).add_to(m)
  168. elif st.session_state.get("temp_lat") is not None:
  169. folium.CircleMarker(location=[st.session_state.temp_lat, st.session_state.temp_lng], radius=m_size+2, color="blue", fill=True, zindex=500).add_to(m)
  170. out = st_folium(m, height=600, width=None, key=f"map_v24_{floor_id}_{tool}")
  171. click = out.get("last_clicked")
  172. if click:
  173. clat, clng = click["lat"], click["lng"]
  174. if tool == "Calibra":
  175. if len(st.session_state.get("cal_points", [])) < 2:
  176. st.session_state.cal_points.append([clat, clng]); st.rerun()
  177. else:
  178. if tool == "Rileva" and snap_on and meta["calibrated"]:
  179. px_step = grid_cm * meta["pixel_ratio"]
  180. st.session_state.temp_lng = meta["origin"][0] + round((clng - meta["origin"][0]) / px_step) * px_step
  181. st.session_state.temp_lat = meta["origin"][1] + round((clat - meta["origin"][1]) / px_step) * px_step
  182. else:
  183. st.session_state.temp_lat, st.session_state.temp_lng = clat, clng
  184. st.rerun()
  185. # --- 6. LOGICA UI ---
  186. with col_ui:
  187. st.write(f"### Tool: **{tool}**")
  188. if tool == "Calibra":
  189. pts = st.session_state.get("cal_points", [])
  190. if len(pts) == 2:
  191. dist_cm = st.number_input("Distanza reale (cm):", value=100.0)
  192. if st.button("📏 SALVA SCALA", use_container_width=True, type="primary"):
  193. px_d = math.sqrt((pts[1][1]-pts[0][1])**2 + (pts[1][0]-pts[0][0])**2)
  194. meta.update({"pixel_ratio": px_d / dist_cm, "calibrated": True})
  195. if force_save_json(meta_path, meta): st.session_state.cal_points = []; st.rerun()
  196. elif st.session_state.get("temp_lat") is not None:
  197. px_x, px_y = st.session_state.temp_lng, st.session_state.temp_lat
  198. if tool == "Origine":
  199. st.metric("X (px)", int(px_x)); st.metric("Y (px)", int(px_y))
  200. if st.button("💾 SALVA ORIGINE", use_container_width=True, type="primary"):
  201. meta["origin"] = [int(px_x), int(px_y)]
  202. if force_save_json(meta_path, meta): st.rerun()
  203. elif tool == "Rileva" and meta["calibrated"]:
  204. rx = (px_x - meta["origin"][0]) / meta["pixel_ratio"]
  205. ry = (meta["origin"][1] - px_y) / meta["pixel_ratio"]
  206. sx, sy = int(round(rx)), int(round(ry))
  207. # --- OTTIMIZZAZIONE SPAZIO: X e Y SU STESSA RIGA ---
  208. c_c1, c_c2 = st.columns(2)
  209. c_c1.metric("X (cm)", sx)
  210. c_c2.metric("Y (cm)", sy)
  211. # Divisore compresso con margine ridotto
  212. st.markdown("<hr style='margin: 0.5em 0;'>", unsafe_allow_html=True)
  213. job_mode = st.radio("Scopo Rilevamento:", ["Addestramento", "Test"], horizontal=True, key="job_mode_v24")
  214. # --- STILE CSS PER OTTIMIZZAZIONE TABLET (Tapping) ---
  215. st.markdown("""
  216. <style>
  217. [data-testid="stRadio"] > div {
  218. gap: 1.0rem;
  219. }
  220. [data-testid="stRadio"] label {
  221. font-size: 19px !important;
  222. padding: 5px 0px;
  223. }
  224. [data-testid="stRadio"] div[role="radiogroup"] > div {
  225. background-color: #f0f2f6;
  226. padding: 4px 12px;
  227. border-radius: 8px;
  228. margin-bottom: 3px;
  229. }
  230. /* Riduzione whitespace globale colonna UI */
  231. .element-container { margin-bottom: 0.3rem !important; }
  232. </style>
  233. """, unsafe_allow_html=True)
  234. if Path(GROUPS_FILE).exists():
  235. g_df = pd.read_csv(Path(GROUPS_FILE), sep=CSV_DELIM)
  236. list_groups = [f"Gruppo: {n}" for n in g_df['BeaconGroupName']]
  237. else:
  238. list_groups = []
  239. if not list_groups:
  240. st.warning("⚠️ Configura i Gruppi.")
  241. else:
  242. # Selezione target via radio per evitare tastiera
  243. sel_target = st.radio("🎯 Selezione Target (Solo Gruppi):", list_groups, key="sel_target_v24_touch")
  244. if st.button("🚀 REGISTRA JOB", use_container_width=True, type="primary"):
  245. sub_dir = "jobs" if job_mode == "Addestramento" else "testjobs"
  246. current_pending = Path(f"/data/train/{sub_dir}/pending")
  247. current_pending.mkdir(parents=True, exist_ok=True)
  248. b_df = pd.read_csv(Path(BEACONS_FILE), sep=CSV_DELIM) if Path(BEACONS_FILE).exists() else pd.DataFrame(columns=['BeaconName','MAC'])
  249. beacon_name_map = {row['MAC']: row['BeaconName'] for _, row in b_df.iterrows()}
  250. job_rows = []
  251. g_name = sel_target.replace("Gruppo: ", "")
  252. g_df = pd.read_csv(Path(GROUPS_FILE), sep=CSV_DELIM)
  253. macs_str = g_df[g_df['BeaconGroupName'] == g_name]['GroupMAC'].iloc[0]
  254. mac_list = [m.strip() for m in macs_str.split(',')]
  255. job_filename = f"{g_name}_{floor_id}_{sx}_{sy}.csv"
  256. for m in mac_list:
  257. b_name = beacon_name_map.get(m, g_name)
  258. pos_id = f"{b_name}_{floor_id}_{sx}_{sy}"
  259. job_rows.append({"Position": pos_id, "Floor": floor_id, "RoomName": b_name, "X": sx, "Y": sy, "Z": floor_id, "BeaconName": b_name, "MAC": m})
  260. header = ["Position", "Floor", "RoomName", "X", "Y", "Z", "BeaconName", "MAC"]
  261. pd.DataFrame(job_rows)[header].to_csv(current_pending / job_filename, sep=CSV_DELIM, index=False)
  262. st.success(f"Job registrato!")
  263. time.sleep(0.5)
  264. st.rerun()
  265. # Pulizia errori nel menu laterale
  266. err_files = list(ERROR_DIR.glob("*.csv")) + list(TEST_ERROR_DIR.glob("*.csv"))
  267. if err_files and st.sidebar.button(f"🗑️ PULISCI ERRORI ({len(err_files)})"):
  268. for f in err_files: os.remove(f)
  269. st.rerun()