25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

296 lines
15 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. MAPS_DIR = Path(cfg['maps']['map_dir'])
  33. # Riferimenti directory per Addestramento e Test
  34. TRAIN_BASE = Path("/data/train")
  35. SAMPLES_DIR = TRAIN_BASE / "samples"
  36. TEST_SAMPLES_DIR = TRAIN_BASE / "testsamples"
  37. JOBS_BASE = Path(cfg['collect_train']['jobs_dir'])
  38. PENDING_DIR = JOBS_BASE / "pending"
  39. ERROR_DIR = JOBS_BASE / "error"
  40. TEST_PENDING_DIR = TRAIN_BASE / "testjobs/pending"
  41. TEST_ERROR_DIR = TRAIN_BASE / "testjobs/error"
  42. # File configurazione
  43. BEACONS_FILE = "/data/config/beacons.csv"
  44. GROUPS_FILE = "/data/config/beacongroup.csv"
  45. CSV_DELIM = cfg.get('paths', {}).get('csv_delimiter', ';')
  46. # Creazione directory se mancano
  47. for p in [MAPS_DIR, SAMPLES_DIR, TEST_SAMPLES_DIR, PENDING_DIR, ERROR_DIR, TEST_PENDING_DIR, TEST_ERROR_DIR]:
  48. p.mkdir(parents=True, exist_ok=True)
  49. # --- 1. GESTIONE UPLOAD ---
  50. maps = sorted([f.replace(cfg['maps']['floor_prefix'], "").split('.')[0]
  51. for f in os.listdir(MAPS_DIR) if f.startswith(cfg['maps']['floor_prefix'])])
  52. with st.expander("📂 Carica Nuova Planimetria", expanded=not maps):
  53. c_up1, c_up2 = st.columns([1, 2])
  54. new_f_id = c_up1.text_input("ID Piano (es. 0, -1):", key="new_f_id_v21")
  55. up_file = c_up2.file_uploader("Immagine (PNG/JPG):", type=['png', 'jpg', 'jpeg'], key="up_v21")
  56. if st.button("🚀 SALVA PIANO", use_container_width=True):
  57. if new_f_id and up_file:
  58. ext = Path(up_file.name).suffix
  59. img_save_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{new_f_id}{ext}"
  60. with open(img_save_path, "wb") as f:
  61. f.write(up_file.getbuffer())
  62. m_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{new_f_id}.json"
  63. force_save_json(m_path, {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0], "grid_size": 50})
  64. st.session_state.current_floor = new_f_id
  65. st.rerun()
  66. if not maps: return
  67. # --- 2. RIGA STATO ---
  68. c_p, c_s, c_o = st.columns([2.5, 2, 2])
  69. with c_p:
  70. s1, s2 = st.columns([1, 1.2])
  71. s1.markdown("<p style='padding-top:35px; font-weight:bold;'>Piano Attivo:</p>", unsafe_allow_html=True)
  72. def_idx = maps.index(st.session_state.current_floor) if st.session_state.get("current_floor") in maps else 0
  73. floor_id = s2.selectbox("", maps, index=def_idx, key="f_v21", label_visibility="collapsed")
  74. if st.session_state.get("prev_floor") != floor_id:
  75. st.session_state.cal_points = []
  76. st.session_state.temp_lat, st.session_state.temp_lng = None, None
  77. st.session_state.prev_floor = floor_id
  78. meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json"
  79. if meta_path.exists():
  80. with open(meta_path, "r") as f: meta = json.load(f)
  81. else:
  82. meta = {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0]}
  83. with c_s: st.info(f"📏 Scala: {'✅' if meta['calibrated'] else '❌'}\n({meta['pixel_ratio']:.4f} px/cm)")
  84. with c_o: st.info(f"🎯 Origine: {'✅' if meta['origin'] != [0,0] else '❌'}\n(X:{meta['origin'][0]}, Y:{meta['origin'][1]})")
  85. # --- 3. IMPOSTAZIONI GLOBALI ---
  86. st.markdown("---")
  87. g1, g2, g3, g4 = st.columns([1.2, 1.5, 1.2, 2])
  88. with g1: show_grid = st.toggle("Griglia", value=False, key="grid_v21")
  89. with g2: grid_cm = st.select_slider("Passo (cm):", options=[25, 50, 100, 200], value=50, key="step_v21")
  90. with g3: snap_on = st.toggle("Stay Grid", value=False, key="snap_v21")
  91. with g4: m_size = st.slider("Dimensione Marker:", 5, 20, 8, key="msize_v21")
  92. st.markdown("---")
  93. # --- 4. TOOLSET ---
  94. t1, t2, t3 = st.columns(3)
  95. if t1.button("📏 CALIBRA", use_container_width=True): st.session_state.map_tool = "Calibra"; st.session_state.cal_points = []
  96. if t2.button("🎯 SET ORIGINE", use_container_width=True): st.session_state.map_tool = "Origine"
  97. if t3.button("📡 RILEVA", use_container_width=True): st.session_state.map_tool = "Rileva"
  98. tool = st.session_state.get('map_tool', 'Rileva')
  99. # --- 5. MAPPA ---
  100. col_map, col_ui = st.columns([3, 1])
  101. with col_map:
  102. 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()))
  103. img_data, w, h = get_image_base64(img_p)
  104. bounds = [[0, 0], [h, w]]
  105. m = folium.Map(location=[h/2, w/2], crs="Simple", tiles=None, attribution_control=False)
  106. m.fit_bounds(bounds)
  107. m.options.update({"minZoom": -6, "maxZoom": 6, "zoomSnap": 0.25, "maxBounds": bounds, "maxBoundsViscosity": 1.0})
  108. folium.raster_layers.ImageOverlay(image=img_data, bounds=bounds, interactive=True).add_to(m)
  109. if meta["origin"] != [0, 0]:
  110. ox, oy = meta["origin"]
  111. folium.CircleMarker(location=[oy, ox], radius=6, color="black", fill=True, zindex=1000).add_to(m)
  112. if show_grid and meta["calibrated"] and meta["origin"] != [0, 0]:
  113. px_step = grid_cm * meta["pixel_ratio"]
  114. ox, oy = meta["origin"]
  115. for x in sorted(list(range(int(ox), w, int(px_step))) + list(range(int(ox), 0, -int(px_step)))):
  116. folium.PolyLine([[0, x], [h, x]], color="blue", weight=1, opacity=0.1).add_to(m)
  117. for y in sorted(list(range(int(oy), h, int(px_step))) + list(range(int(oy), 0, -int(px_step)))):
  118. folium.PolyLine([[y, 0], [y, w]], color="blue", weight=1, opacity=0.1).add_to(m)
  119. # --- DISEGNO PUNTI STORICI (OTTIMIZZATO TRAMITE NOME FILE) ---
  120. def draw_historical_samples(directory, color, shape="circle", is_test=False):
  121. if not meta["calibrated"] or meta["origin"] == [0,0]: return
  122. for f in Path(directory).glob("*.csv"):
  123. try:
  124. # Parsing nome file: CAMPAGNA_Z_X_Y.csv
  125. # Usiamo parts[-1], parts[-2], parts[-3] per gestire campagne con underscore
  126. parts = f.stem.split('_')
  127. if len(parts) < 4: continue
  128. fz = parts[-3] # Piano Z
  129. fx = float(parts[-2].replace(',', '.')) # Coordinata X
  130. fy = float(parts[-1].replace(',', '.')) # Coordinata Y
  131. if str(fz) == str(floor_id):
  132. px_x = (fx * meta["pixel_ratio"]) + meta["origin"][0]
  133. px_y = meta["origin"][1] - (fy * meta["pixel_ratio"])
  134. dash = "5, 5" if is_test else None
  135. weight = 3 if is_test else 1
  136. if shape == "circle":
  137. folium.CircleMarker(
  138. location=[px_y, px_x], radius=m_size, color=color,
  139. weight=weight, dash_array=dash, fill=True, fill_opacity=0.7
  140. ).add_to(m)
  141. except: continue
  142. # 1. ADDESTRAMENTO: Verde, Tondo, Solido
  143. draw_historical_samples(SAMPLES_DIR, "green", "circle", is_test=False)
  144. # 2. TEST: Celeste (#00FFFF), Tondo, Tratteggiato
  145. draw_historical_samples(TEST_SAMPLES_DIR, "#00FFFF", "circle", is_test=True)
  146. # 3. PENDING/ERROR: (Manteniamo la lettura CSV per questi perché i nomi file sono diversi)
  147. def draw_jobs(directory, color, shape="diamond", is_test=False):
  148. if not meta["calibrated"] or meta["origin"] == [0,0]: return
  149. for f in Path(directory).glob("*.csv"):
  150. try:
  151. df = pd.read_csv(f, sep=";")
  152. df.columns = [c.lower() for c in df.columns]
  153. row = df.iloc[0]
  154. z_val = str(row.get('z', row.get('floor')))
  155. if z_val == str(floor_id):
  156. px_x = (row['x'] * meta["pixel_ratio"]) + meta["origin"][0]
  157. px_y = meta["origin"][1] - (row['y'] * meta["pixel_ratio"])
  158. dash = "3, 3" if is_test else None
  159. folium.RegularPolygonMarker(
  160. location=[px_y, px_x], number_of_sides=4,
  161. rotation=45 if shape=="diamond" else 0,
  162. radius=m_size, color=color, weight=2,
  163. dash_array=dash, fill=True
  164. ).add_to(m)
  165. except: continue
  166. draw_jobs(PENDING_DIR, "orange", "diamond", False)
  167. draw_jobs(TEST_PENDING_DIR, "#f1c40f", "diamond", True)
  168. draw_jobs(ERROR_DIR, "red", "square", False)
  169. draw_jobs(TEST_ERROR_DIR, "#e74c3c", "square", True)
  170. if tool == "Calibra" and st.session_state.get("cal_points"):
  171. for p in st.session_state.cal_points: folium.CircleMarker(location=p, radius=6, color="red", fill=True).add_to(m)
  172. if len(st.session_state.cal_points) == 2: folium.PolyLine(st.session_state.cal_points, color="red", weight=3).add_to(m)
  173. elif st.session_state.get("temp_lat") is not None:
  174. 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)
  175. out = st_folium(m, height=600, width=None, key=f"map_v24_{floor_id}_{tool}")
  176. click = out.get("last_clicked")
  177. if click:
  178. clat, clng = click["lat"], click["lng"]
  179. if tool == "Calibra":
  180. if len(st.session_state.get("cal_points", [])) < 2:
  181. st.session_state.cal_points.append([clat, clng]); st.rerun()
  182. else:
  183. if tool == "Rileva" and snap_on and meta["calibrated"]:
  184. px_step = grid_cm * meta["pixel_ratio"]
  185. st.session_state.temp_lng = meta["origin"][0] + round((clng - meta["origin"][0]) / px_step) * px_step
  186. st.session_state.temp_lat = meta["origin"][1] + round((clat - meta["origin"][1]) / px_step) * px_step
  187. else:
  188. st.session_state.temp_lat, st.session_state.temp_lng = clat, clng
  189. st.rerun()
  190. # --- 6. LOGICA UI ---
  191. with col_ui:
  192. st.write(f"### Tool: **{tool}**")
  193. if tool == "Calibra":
  194. pts = st.session_state.get("cal_points", [])
  195. if len(pts) == 2:
  196. dist_cm = st.number_input("Distanza reale (cm):", value=100.0)
  197. if st.button("📏 SALVA SCALA", use_container_width=True, type="primary"):
  198. px_d = math.sqrt((pts[1][1]-pts[0][1])**2 + (pts[1][0]-pts[0][0])**2)
  199. meta.update({"pixel_ratio": px_d / dist_cm, "calibrated": True})
  200. if force_save_json(meta_path, meta): st.session_state.cal_points = []; st.rerun()
  201. elif st.session_state.get("temp_lat") is not None:
  202. px_x, px_y = st.session_state.temp_lng, st.session_state.temp_lat
  203. if tool == "Origine":
  204. st.metric("X (px)", int(px_x)); st.metric("Y (px)", int(px_y))
  205. if st.button("💾 SALVA ORIGINE", use_container_width=True, type="primary"):
  206. meta["origin"] = [int(px_x), int(px_y)]
  207. if force_save_json(meta_path, meta): st.rerun()
  208. elif tool == "Rileva" and meta["calibrated"]:
  209. rx = (px_x - meta["origin"][0]) / meta["pixel_ratio"]
  210. ry = (meta["origin"][1] - px_y) / meta["pixel_ratio"]
  211. sx, sy = int(round(rx)), int(round(ry))
  212. st.metric("X (cm)", sx); st.metric("Y (cm)", sy)
  213. st.divider()
  214. job_mode = st.radio("Scopo Rilevamento:", ["Addestramento", "Test"], horizontal=True, key="job_mode_v24")
  215. b_df = pd.read_csv(Path(BEACONS_FILE), sep=CSV_DELIM) if Path(BEACONS_FILE).exists() else pd.DataFrame(columns=['BeaconName','MAC'])
  216. beacon_name_map = {row['MAC']: row['BeaconName'] for _, row in b_df.iterrows()}
  217. options = [f"Beacon: {n} | {m}" for n, m in zip(b_df['BeaconName'], b_df['MAC'])]
  218. if Path(GROUPS_FILE).exists():
  219. g_df = pd.read_csv(Path(GROUPS_FILE), sep=CSV_DELIM)
  220. options += [f"Gruppo: {n}" for n in g_df['BeaconGroupName']]
  221. sel_target = st.selectbox("Seleziona Target:", options, key="sel_target_v24")
  222. if st.button("🚀 REGISTRA JOB", use_container_width=True, type="primary"):
  223. sub_dir = "jobs" if job_mode == "Addestramento" else "testjobs"
  224. current_pending = Path(f"/data/train/{sub_dir}/pending")
  225. current_pending.mkdir(parents=True, exist_ok=True)
  226. job_rows = []
  227. if sel_target.startswith("Gruppo: "):
  228. g_name = sel_target.replace("Gruppo: ", "")
  229. g_df = pd.read_csv(Path(GROUPS_FILE), sep=CSV_DELIM)
  230. macs_str = g_df[g_df['BeaconGroupName'] == g_name]['GroupMAC'].iloc[0]
  231. mac_list = [m.strip() for m in macs_str.split(',')]
  232. job_filename = f"{g_name}_{floor_id}_{sx}_{sy}.csv"
  233. for m in mac_list:
  234. b_name = beacon_name_map.get(m, g_name)
  235. pos_id = f"{b_name}_{floor_id}_{sx}_{sy}"
  236. job_rows.append({"Position": pos_id, "Floor": floor_id, "RoomName": b_name, "X": sx, "Y": sy, "Z": floor_id, "BeaconName": b_name, "MAC": m})
  237. else:
  238. b_info = sel_target.replace("Beacon: ", "").split(" | ")
  239. b_name, b_mac = b_info[0], b_info[1]
  240. job_filename = f"{b_name}_{floor_id}_{sx}_{sy}.csv"
  241. pos_id = f"{b_name}_{floor_id}_{sx}_{sy}"
  242. job_rows.append({"Position": pos_id, "Floor": floor_id, "RoomName": b_name, "X": sx, "Y": sy, "Z": floor_id, "BeaconName": b_name, "MAC": b_mac})
  243. header = ["Position", "Floor", "RoomName", "X", "Y", "Z", "BeaconName", "MAC"]
  244. pd.DataFrame(job_rows)[header].to_csv(current_pending / job_filename, sep=CSV_DELIM, index=False)
  245. st.success(f"Job registrato!")
  246. time.sleep(0.5)
  247. st.rerun()
  248. # Pulizia errori nel menu laterale
  249. err_files = list(ERROR_DIR.glob("*.csv")) + list(TEST_ERROR_DIR.glob("*.csv"))
  250. if err_files and st.sidebar.button(f"🗑️ PULISCI ERRORI ({len(err_files)})"):
  251. for f in err_files: os.remove(f)
  252. st.rerun()