propix-gui-analysis 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
File without changes
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import base64
|
|
3
|
+
import numpy as np
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from PIL import Image
|
|
6
|
+
from typing import Dict, Any, List
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# --------------------------------------------------
|
|
12
|
+
# Utils
|
|
13
|
+
# --------------------------------------------------
|
|
14
|
+
|
|
15
|
+
def _decode_base64_image(data: str, mode: str) -> np.ndarray:
|
|
16
|
+
"""
|
|
17
|
+
Décode une image base64 en numpy array.
|
|
18
|
+
mode = "RGB" ou "L"
|
|
19
|
+
"""
|
|
20
|
+
decoded = base64.b64decode(data.strip())
|
|
21
|
+
img = Image.open(BytesIO(decoded)).convert(mode)
|
|
22
|
+
return np.array(img)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# --------------------------------------------------
|
|
26
|
+
# Main loader
|
|
27
|
+
# --------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def load_frame(json_path: str) -> Dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Charge un frame JSON PROPIX et retourne un dict Python
|
|
32
|
+
100 % exploitable (numpy-ready).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
36
|
+
raw = json.load(f)
|
|
37
|
+
|
|
38
|
+
# --- RGB ---
|
|
39
|
+
rgb = None
|
|
40
|
+
if raw.get("rgb_img") is not None:
|
|
41
|
+
rgb = _decode_base64_image(raw["rgb_img"], mode="RGB")
|
|
42
|
+
|
|
43
|
+
# --- Masks ---
|
|
44
|
+
masks = []
|
|
45
|
+
if raw.get("masks") is not None:
|
|
46
|
+
for mask_b64 in raw["masks"]:
|
|
47
|
+
mask = _decode_base64_image(mask_b64, mode="L")
|
|
48
|
+
mask = mask.astype(np.float32) / 255.0 # normalisé [0,1]
|
|
49
|
+
masks.append(mask)
|
|
50
|
+
|
|
51
|
+
# --- Colors ---
|
|
52
|
+
mask_colors = raw.get("mask_colors", [])
|
|
53
|
+
mask_colors = [
|
|
54
|
+
[c / 255 if max(color) > 1 else c for c in color]
|
|
55
|
+
for color in mask_colors
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
frame = {
|
|
59
|
+
# Metadata
|
|
60
|
+
"frame_id": raw.get("frame"),
|
|
61
|
+
"task_finished": raw.get("taskFinished"),
|
|
62
|
+
|
|
63
|
+
# Spectral
|
|
64
|
+
"wavelengths": np.array(raw.get("wavelengths")) if raw.get("wavelengths") else None,
|
|
65
|
+
"spectra": {
|
|
66
|
+
"values": np.array(raw.get("spectra")) if raw.get("spectra") else None,
|
|
67
|
+
"labels": raw.get("labels"),
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
# Image
|
|
71
|
+
"rgb": rgb, # np.ndarray [H,W,3]
|
|
72
|
+
"masks": masks, # List[np.ndarray [H,W]]
|
|
73
|
+
"mask_colors": mask_colors, # List[RGBA] normalisés
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return frame
|
|
77
|
+
|
|
78
|
+
from typing import Dict, Any
|
|
79
|
+
import numpy as np
|
|
80
|
+
|
|
81
|
+
def frame_summary(frame: Dict[str, Any]) -> None:
|
|
82
|
+
print("\n" + "=" * 48)
|
|
83
|
+
print(" PROPIX FRAME — SUMMARY")
|
|
84
|
+
print("=" * 48)
|
|
85
|
+
|
|
86
|
+
# --- Image ---
|
|
87
|
+
rgb = frame.get("rgb")
|
|
88
|
+
if rgb is not None:
|
|
89
|
+
h, w = rgb.shape[:2]
|
|
90
|
+
total_pixels = h * w
|
|
91
|
+
print("\n🖼️ Image")
|
|
92
|
+
print(f" • Resolution : {w} x {h} px")
|
|
93
|
+
print(f" • Total pixels : {total_pixels:,}")
|
|
94
|
+
else:
|
|
95
|
+
print("\n🖼️ Image : ❌ missing")
|
|
96
|
+
total_pixels = None
|
|
97
|
+
|
|
98
|
+
# --- Masks ---
|
|
99
|
+
masks = frame.get("masks", [])
|
|
100
|
+
print("\n🎭 Masks")
|
|
101
|
+
print(f" • Count : {len(masks)}")
|
|
102
|
+
|
|
103
|
+
if masks and total_pixels is not None:
|
|
104
|
+
percentages = []
|
|
105
|
+
|
|
106
|
+
for m in masks:
|
|
107
|
+
nonzero_pixels = int(np.count_nonzero(m))
|
|
108
|
+
pct = 100.0 * nonzero_pixels / total_pixels
|
|
109
|
+
percentages.append(pct)
|
|
110
|
+
|
|
111
|
+
print(
|
|
112
|
+
f" • Masked area : "
|
|
113
|
+
f"min={min(percentages):.2f}% | "
|
|
114
|
+
f"max={max(percentages):.2f}%"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# --- Spectral ---
|
|
118
|
+
wavelengths = frame.get("wavelengths")
|
|
119
|
+
print("\n🌈 Spectral")
|
|
120
|
+
if wavelengths is not None and len(wavelengths) > 1:
|
|
121
|
+
wl_min = float(wavelengths.min())
|
|
122
|
+
wl_max = float(wavelengths.max())
|
|
123
|
+
bands = len(wavelengths)
|
|
124
|
+
|
|
125
|
+
# mean spectral resolution (Δλ)
|
|
126
|
+
spectral_resolution = float(np.mean(np.diff(wavelengths)))
|
|
127
|
+
|
|
128
|
+
print(f" • Wavelength range : {wl_min:.1f} – {wl_max:.1f} nm")
|
|
129
|
+
print(f" • Bands : {bands}")
|
|
130
|
+
print(f" • Spectral resolution: {spectral_resolution:.2f} nm")
|
|
131
|
+
else:
|
|
132
|
+
print(" • Spectral data : ❌ missing")
|
|
133
|
+
|
|
134
|
+
print("\n" + "=" * 48 + "\n")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def plot_rgb_with_masks(frame, alpha=0.3):
|
|
139
|
+
import numpy as np
|
|
140
|
+
import matplotlib.pyplot as plt
|
|
141
|
+
|
|
142
|
+
rgb = frame["rgb"]
|
|
143
|
+
masks = frame["masks"]
|
|
144
|
+
colors = frame["mask_colors"]
|
|
145
|
+
|
|
146
|
+
plt.figure(figsize=(6, 6))
|
|
147
|
+
plt.imshow(rgb)
|
|
148
|
+
plt.title(f"RGB + masks – Frame {frame['frame_id']}")
|
|
149
|
+
plt.axis("off")
|
|
150
|
+
|
|
151
|
+
for i, mask in enumerate(masks):
|
|
152
|
+
color = colors[i]
|
|
153
|
+
|
|
154
|
+
overlay = np.zeros((*mask.shape, 4), dtype=np.float32)
|
|
155
|
+
overlay[..., :3] = color[:3] # couleur
|
|
156
|
+
overlay[..., 3] = mask * alpha # alpha maîtrisé
|
|
157
|
+
|
|
158
|
+
plt.imshow(overlay)
|
|
159
|
+
|
|
160
|
+
plt.tight_layout()
|
|
161
|
+
plt.show()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def plot_rgb(frame: Dict[str, Any]) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Affiche uniquement l'image RGB d'un frame.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
rgb = frame.get("rgb")
|
|
170
|
+
|
|
171
|
+
if rgb is None:
|
|
172
|
+
print("❌ Pas d'image RGB dans ce frame")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
plt.figure(figsize=(6, 6))
|
|
176
|
+
plt.imshow(rgb)
|
|
177
|
+
plt.axis("off")
|
|
178
|
+
plt.tight_layout()
|
|
179
|
+
plt.show()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
import numpy as np
|
|
184
|
+
import matplotlib.pyplot as plt
|
|
185
|
+
from typing import Dict, Any
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def plot_masks_only(
|
|
189
|
+
frame: Dict[str, Any],
|
|
190
|
+
alpha: float = 0.5,
|
|
191
|
+
superpose: bool = True
|
|
192
|
+
) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Affiche uniquement les masques (sans image RGB).
|
|
195
|
+
|
|
196
|
+
- superpose=True : masques colorés superposés
|
|
197
|
+
- superpose=False : un masque par subplot en noir/blanc
|
|
198
|
+
(1 = blanc, 0 = noir)
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
masks = frame.get("masks", [])
|
|
202
|
+
colors = frame.get("mask_colors", [])
|
|
203
|
+
|
|
204
|
+
if not masks:
|
|
205
|
+
print("❌ Aucun masque à afficher")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# ============================
|
|
209
|
+
# CAS 1 : superposition colorée
|
|
210
|
+
# ============================
|
|
211
|
+
if superpose:
|
|
212
|
+
plt.figure(figsize=(6, 6))
|
|
213
|
+
plt.axis("off")
|
|
214
|
+
|
|
215
|
+
for i, mask in enumerate(masks):
|
|
216
|
+
if mask is None:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
color = colors[i] if i < len(colors) else [1.0, 0.0, 0.0, 1.0]
|
|
220
|
+
|
|
221
|
+
overlay = np.zeros((*mask.shape, 4), dtype=np.float32)
|
|
222
|
+
overlay[..., :3] = color[:3]
|
|
223
|
+
overlay[..., 3] = mask * alpha
|
|
224
|
+
|
|
225
|
+
plt.imshow(overlay)
|
|
226
|
+
|
|
227
|
+
plt.title("Masques superposés")
|
|
228
|
+
plt.tight_layout()
|
|
229
|
+
plt.show()
|
|
230
|
+
|
|
231
|
+
# ============================
|
|
232
|
+
# CAS 2 : un masque par subplot
|
|
233
|
+
# ============================
|
|
234
|
+
else:
|
|
235
|
+
valid_masks = [(i, m) for i, m in enumerate(masks) if m is not None]
|
|
236
|
+
n = len(valid_masks)
|
|
237
|
+
|
|
238
|
+
cols = int(np.ceil(np.sqrt(n)))
|
|
239
|
+
rows = int(np.ceil(n / cols))
|
|
240
|
+
|
|
241
|
+
fig, axes = plt.subplots(rows, cols, figsize=(4 * cols, 4 * rows))
|
|
242
|
+
axes = np.array(axes).reshape(-1)
|
|
243
|
+
|
|
244
|
+
for ax, (i, mask) in zip(axes, valid_masks):
|
|
245
|
+
ax.imshow(mask, cmap="gray", vmin=0, vmax=1)
|
|
246
|
+
ax.set_title(f"Masque {i}", fontsize=14)
|
|
247
|
+
ax.axis("off")
|
|
248
|
+
|
|
249
|
+
# Désactiver les axes inutilisés
|
|
250
|
+
for ax in axes[len(valid_masks):]:
|
|
251
|
+
ax.axis("off")
|
|
252
|
+
|
|
253
|
+
plt.suptitle("Masques individuels (binaire)", fontsize=16)
|
|
254
|
+
plt.tight_layout()
|
|
255
|
+
plt.show()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def plot_frame_rgb_and_spectra(frame: Dict[str, Any], alpha: float = 0.3):
|
|
260
|
+
"""
|
|
261
|
+
Affiche :
|
|
262
|
+
- à gauche : RGB + masques en transparence
|
|
263
|
+
- à droite : spectres (intensité vs wavelengths)
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
rgb = frame["rgb"]
|
|
267
|
+
masks = frame["masks"]
|
|
268
|
+
colors = frame["mask_colors"]
|
|
269
|
+
|
|
270
|
+
wavelengths = frame["wavelengths"]
|
|
271
|
+
spectra = frame["spectra"]["values"]
|
|
272
|
+
labels = frame["spectra"]["labels"]
|
|
273
|
+
|
|
274
|
+
fig, (ax_img, ax_spec) = plt.subplots(
|
|
275
|
+
1, 2, figsize=(12, 5), gridspec_kw={"width_ratios": [1, 1]}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# -------------------------------------------------
|
|
279
|
+
# 🖼️ RGB + masks
|
|
280
|
+
# -------------------------------------------------
|
|
281
|
+
ax_img.imshow(rgb)
|
|
282
|
+
ax_img.axis("off")
|
|
283
|
+
|
|
284
|
+
for i, mask in enumerate(masks):
|
|
285
|
+
color = colors[i]
|
|
286
|
+
|
|
287
|
+
overlay = np.zeros((*mask.shape, 4), dtype=np.float32)
|
|
288
|
+
overlay[..., :3] = color[:3]
|
|
289
|
+
overlay[..., 3] = mask * alpha
|
|
290
|
+
|
|
291
|
+
ax_img.imshow(overlay)
|
|
292
|
+
|
|
293
|
+
# -------------------------------------------------
|
|
294
|
+
# 📈 Spectres
|
|
295
|
+
# -------------------------------------------------
|
|
296
|
+
for i, spectrum in enumerate(spectra):
|
|
297
|
+
label = f"Spectrum {i}"
|
|
298
|
+
if labels:
|
|
299
|
+
label += f" ({labels[i]})"
|
|
300
|
+
|
|
301
|
+
ax_spec.plot(wavelengths, spectrum, label=label)
|
|
302
|
+
|
|
303
|
+
ax_spec.set_xlabel("Wavelength")
|
|
304
|
+
ax_spec.set_ylabel("Intensity")
|
|
305
|
+
ax_spec.set_title("Spectra")
|
|
306
|
+
ax_spec.grid(True)
|
|
307
|
+
ax_spec.legend()
|
|
308
|
+
|
|
309
|
+
plt.tight_layout()
|
|
310
|
+
plt.show()
|
|
311
|
+
|
|
312
|
+
def plot_spectra(frame: Dict[str, Any]) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Affiche les spectres (intensité vs longueur d'onde).
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
wavelengths = frame.get("wavelengths")
|
|
319
|
+
spectra = frame.get("spectra", {}).get("values")
|
|
320
|
+
labels = frame.get("spectra", {}).get("labels")
|
|
321
|
+
|
|
322
|
+
if wavelengths is None or spectra is None:
|
|
323
|
+
print("❌ Données spectrales manquantes")
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
plt.figure(figsize=(7, 4))
|
|
327
|
+
|
|
328
|
+
for i, spectrum in enumerate(spectra):
|
|
329
|
+
label = f"Spectrum {i}"
|
|
330
|
+
if labels and i < len(labels):
|
|
331
|
+
label += f" ({labels[i]})"
|
|
332
|
+
|
|
333
|
+
plt.plot(wavelengths, spectrum, label=label)
|
|
334
|
+
|
|
335
|
+
plt.xlabel("Wavelength")
|
|
336
|
+
plt.ylabel("Intensity")
|
|
337
|
+
plt.grid(True)
|
|
338
|
+
plt.legend()
|
|
339
|
+
plt.tight_layout()
|
|
340
|
+
plt.show()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: propix_gui_analysis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GUI analysis tools for Propix JSON processing
|
|
5
|
+
Author-email: Gaspard Russias <grussias@photonics-open-projects.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: numpy>=1.23
|
|
10
|
+
Requires-Dist: matplotlib>=3.7
|
|
11
|
+
Requires-Dist: pillow>=9.0
|
|
12
|
+
Requires-Dist: packaging>=23.0
|
|
13
|
+
Requires-Dist: python-dateutil>=2.8
|
|
14
|
+
Requires-Dist: six>=1.16
|
|
15
|
+
|
|
16
|
+
# PROPIX GUI Analysis
|
|
17
|
+
|
|
18
|
+
A lightweight Python library to **load, inspect, summarize, and visualize JSON frames generated by the PROPIX GUI**.
|
|
19
|
+
|
|
20
|
+
This package is designed to work directly with PROPIX GUI outputs, including:
|
|
21
|
+
- RGB images
|
|
22
|
+
- Segmentation masks
|
|
23
|
+
- Hyperspectral data (wavelengths and spectra)
|
|
24
|
+
|
|
25
|
+
It provides a clean, NumPy-friendly API for **analysis, debugging, and visualization**.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ✨ Features
|
|
30
|
+
|
|
31
|
+
- Load PROPIX GUI JSON frames into Python structures
|
|
32
|
+
- Decode base64-encoded RGB images and masks
|
|
33
|
+
- Normalize and handle multiple segmentation masks
|
|
34
|
+
- Visualize:
|
|
35
|
+
- RGB images
|
|
36
|
+
- Masks (overlayed or individual)
|
|
37
|
+
- RGB + masks
|
|
38
|
+
- Spectral curves
|
|
39
|
+
- RGB + masks + spectra (side-by-side)
|
|
40
|
+
- Print a **human-readable frame summary**:
|
|
41
|
+
- Image resolution
|
|
42
|
+
- Mask coverage (percentage of pixels)
|
|
43
|
+
- Spectral range
|
|
44
|
+
- Number of spectral bands
|
|
45
|
+
- Spectral resolution (Δλ)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 📦 Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install propix_gui_analysis
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or for development:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install -e .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🚀 Quick Start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from propix_gui_analysis.propix_json_analysis import (
|
|
67
|
+
load_frame,
|
|
68
|
+
frame_summary,
|
|
69
|
+
plot_rgb,
|
|
70
|
+
plot_rgb_with_masks,
|
|
71
|
+
plot_masks_only,
|
|
72
|
+
plot_spectra,
|
|
73
|
+
plot_frame_rgb_and_spectra,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
frame = load_frame("frame.json")
|
|
77
|
+
frame_summary(frame)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 📂 Frame Structure
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
frame = {
|
|
86
|
+
"frame_id": int,
|
|
87
|
+
"task_finished": bool,
|
|
88
|
+
|
|
89
|
+
"rgb": np.ndarray, # [H, W, 3]
|
|
90
|
+
"masks": List[np.ndarray], # list of [H, W] masks (values in [0,1])
|
|
91
|
+
"mask_colors": List[List[float]], # normalized RGBA colors
|
|
92
|
+
|
|
93
|
+
"wavelengths": np.ndarray, # [N]
|
|
94
|
+
"spectra": {
|
|
95
|
+
"values": np.ndarray, # [num_masks, N]
|
|
96
|
+
"labels": List[str]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🖨️ Frame Summary
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
frame_summary(frame)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 📊 Visualization
|
|
112
|
+
|
|
113
|
+
- `plot_rgb(frame)`
|
|
114
|
+
- `plot_rgb_with_masks(frame)`
|
|
115
|
+
- `plot_masks_only(frame)`
|
|
116
|
+
- `plot_spectra(frame)`
|
|
117
|
+
- `plot_frame_rgb_and_spectra(frame)`
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🔬 Spectral Resolution
|
|
122
|
+
|
|
123
|
+
Defined as the **average wavelength spacing (Δλ)** between consecutive bands.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 🧰 Dependencies
|
|
128
|
+
|
|
129
|
+
- Python ≥ 3.8
|
|
130
|
+
- NumPy
|
|
131
|
+
- Pillow
|
|
132
|
+
- Matplotlib
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 📄 License
|
|
137
|
+
|
|
138
|
+
MIT License.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
propix_gui_analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
propix_gui_analysis/propix_gui_analysis.py,sha256=1L1JxOAaQCYGyK1rDZgDFaOhPV9n5fdzBun-nzsdusY,9506
|
|
3
|
+
propix_gui_analysis-0.1.0.dist-info/METADATA,sha256=ppnVh306HCHu2E1sySrf5rLU9uECtCfakLpOSAA1aS8,2775
|
|
4
|
+
propix_gui_analysis-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
propix_gui_analysis-0.1.0.dist-info/top_level.txt,sha256=KhzinIEzpkaqIeIAARxIC1n53hBSGyv4J9sLUnPkcrs,20
|
|
6
|
+
propix_gui_analysis-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
propix_gui_analysis
|