Gunin09 commited on
Commit
ca09663
·
verified ·
1 Parent(s): 02fdaa7

Upload visualizer.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. visualizer.py +196 -0
visualizer.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import plotly.graph_objects as go
3
+
4
+ from data_loader import ARTIFACT_COLORS, ARTIFACT_LABEL_MAP
5
+
6
+ CHANNEL_SPACING = 1.0
7
+
8
+
9
+ def build_eeg_figure(
10
+ signal: np.ndarray,
11
+ channel_names: list[str],
12
+ sampling_rate_hz: float,
13
+ start_time_s: float = 0.0,
14
+ annotations: list[dict] | None = None,
15
+ title: str = "EEG Signal",
16
+ ) -> go.Figure:
17
+ n_channels, n_samples = signal.shape
18
+ time_axis = start_time_s + np.arange(n_samples) / sampling_rate_hz
19
+
20
+ normalized = np.zeros_like(signal, dtype=np.float32)
21
+ for i in range(n_channels):
22
+ p2p = np.nanpercentile(signal[i], 97.5) - np.nanpercentile(signal[i], 2.5)
23
+ if p2p > 0:
24
+ normalized[i] = signal[i] / p2p * CHANNEL_SPACING * 0.8
25
+ else:
26
+ normalized[i] = 0.0
27
+
28
+ fig = go.Figure()
29
+
30
+ if annotations:
31
+ _add_artifact_shapes(fig, annotations, start_time_s, time_axis[-1], n_channels)
32
+
33
+ for i in range(n_channels):
34
+ ch_data = normalized[i]
35
+ offset = -i * CHANNEL_SPACING
36
+ name = channel_names[i] if i < len(channel_names) else f"Ch{i}"
37
+ fig.add_trace(go.Scatter(
38
+ x=time_axis,
39
+ y=ch_data + offset,
40
+ mode="lines",
41
+ name=name,
42
+ line=dict(width=0.8, color=f"hsl({(i * 37) % 360}, 70%, 65%)"),
43
+ hovertemplate=(
44
+ f"<b>{name}</b><br>"
45
+ "Time: %{x:.3f}s<br>"
46
+ "Amplitude: %{customdata:.1f} uV<extra></extra>"
47
+ ),
48
+ customdata=signal[i],
49
+ ))
50
+
51
+ y_ticks = [-i * CHANNEL_SPACING for i in range(n_channels)]
52
+ y_labels = [channel_names[i] if i < len(channel_names) else f"Ch{i}" for i in range(n_channels)]
53
+
54
+ fig.update_layout(
55
+ title=dict(text=title, font=dict(size=14)),
56
+ xaxis=dict(
57
+ title="Time (s)",
58
+ showgrid=True,
59
+ gridcolor="rgba(128,128,128,0.2)",
60
+ zeroline=False,
61
+ dtick=1.0,
62
+ ),
63
+ yaxis=dict(
64
+ tickvals=y_ticks,
65
+ ticktext=y_labels,
66
+ showgrid=False,
67
+ zeroline=False,
68
+ ),
69
+ height=max(500, n_channels * 30 + 120),
70
+ margin=dict(l=110, r=20, t=50, b=50),
71
+ plot_bgcolor="rgb(15, 15, 25)",
72
+ paper_bgcolor="rgb(10, 10, 20)",
73
+ font=dict(color="rgb(200, 200, 210)", size=11),
74
+ showlegend=False,
75
+ hovermode="x unified",
76
+ dragmode="zoom",
77
+ )
78
+
79
+ return fig
80
+
81
+
82
+ def _add_artifact_shapes(
83
+ fig: go.Figure,
84
+ annotations: list[dict],
85
+ view_start: float,
86
+ view_end: float,
87
+ n_channels: int,
88
+ ) -> None:
89
+ y_top = CHANNEL_SPACING
90
+ y_bottom = -(n_channels - 0.5) * CHANNEL_SPACING
91
+
92
+ for ann in annotations:
93
+ onset = max(ann["onset_s"], view_start)
94
+ end = min(ann["end_s"], view_end)
95
+ if onset >= end:
96
+ continue
97
+
98
+ label = ann.get("label", ann.get("raw_label", ""))
99
+ color = ann.get("color", "rgba(128, 128, 128, 0.3)")
100
+ strong_color = color.replace("0.25)", "0.35)")
101
+
102
+ fig.add_shape(
103
+ type="rect",
104
+ x0=onset, x1=end,
105
+ y0=y_bottom, y1=y_top,
106
+ fillcolor=strong_color,
107
+ line=dict(width=1, color=strong_color.replace("0.35)", "0.7)")),
108
+ layer="below",
109
+ )
110
+
111
+ mid_x = (onset + end) / 2
112
+ if (end - onset) > 0.2:
113
+ fig.add_annotation(
114
+ x=mid_x,
115
+ y=y_top + CHANNEL_SPACING * 0.3,
116
+ text=f"<b>{label}</b>",
117
+ showarrow=False,
118
+ font=dict(size=11, color="white"),
119
+ bgcolor=strong_color.replace("0.35)", "0.85)"),
120
+ borderpad=3,
121
+ )
122
+
123
+
124
+ def build_artifact_legend() -> str:
125
+ items = []
126
+ for label, color in ARTIFACT_COLORS.items():
127
+ if label in ("Clean", "Background"):
128
+ continue
129
+ rgba_solid = color.replace("0.25)", "0.85)")
130
+ items.append(
131
+ f'<div style="display:flex;align-items:center;margin:4px 0;">'
132
+ f'<span style="display:inline-block;width:16px;height:16px;'
133
+ f'background:{rgba_solid};border-radius:3px;margin-right:8px;'
134
+ f'flex-shrink:0;"></span>'
135
+ f'<span style="font-size:13px;">{label}</span></div>'
136
+ )
137
+ return '<div style="padding:4px;">' + "".join(items) + "</div>"
138
+
139
+
140
+ def build_metadata_html(info: dict) -> str:
141
+ rows = [
142
+ ("Recording ID", info.get("recording_id", "N/A")),
143
+ ("Dataset", info.get("dataset_id", "N/A")),
144
+ ("Subject", info.get("subject", "N/A")),
145
+ ("Session", info.get("session", "N/A")),
146
+ ("Task", info.get("task", "N/A")),
147
+ ("Duration", f"{info.get('duration_s', 0):.1f} s"),
148
+ ("Channels", f"{info.get('n_channels', 0)} ({info.get('n_eeg_channels', 0)} EEG)"),
149
+ ("Sampling Rate", f"{info.get('sampling_rate_hz', 0):.0f} Hz"),
150
+ ("Reference", info.get("reference", "N/A")),
151
+ ("Montage", info.get("montage_name", "N/A")),
152
+ ("Format", info.get("archival_format", "N/A")),
153
+ ("Roundtrip", info.get("roundtrip_class", "N/A")),
154
+ ]
155
+
156
+ html = '<div style="font-family:monospace; font-size:13px; line-height:1.8;">'
157
+ for label, value in rows:
158
+ html += (
159
+ f'<div><span style="color:#888;min-width:130px;display:inline-block;">'
160
+ f'{label}:</span> <span style="color:#e0e0e0;">{value}</span></div>'
161
+ )
162
+ html += "</div>"
163
+ return html
164
+
165
+
166
+ def build_annotation_summary(annotations: list[dict]) -> str:
167
+ if not annotations:
168
+ return (
169
+ '<div style="color:#888; font-size:13px; padding:8px;">'
170
+ 'No artifact annotations in this window. '
171
+ 'Try navigating to a different time region.</div>'
172
+ )
173
+
174
+ counts: dict[str, int] = {}
175
+ total_dur: dict[str, float] = {}
176
+ for ann in annotations:
177
+ label = ann.get("label", "Unknown")
178
+ counts[label] = counts.get(label, 0) + 1
179
+ total_dur[label] = total_dur.get(label, 0) + ann.get("duration_s", 0)
180
+
181
+ html = '<div style="font-family:monospace; font-size:13px; line-height:1.8; padding:4px;">'
182
+ html += f'<div style="color:#fff; margin-bottom:6px;"><b>Artifacts found: {len(annotations)}</b></div>'
183
+ for label in sorted(counts.keys()):
184
+ color = ARTIFACT_COLORS.get(label, "rgba(128,128,128,0.5)")
185
+ rgba_solid = color.replace("0.25)", "0.85)")
186
+ html += (
187
+ f'<div style="display:flex;align-items:center;margin:2px 0;">'
188
+ f'<span style="display:inline-block;width:12px;height:12px;'
189
+ f'background:{rgba_solid};border-radius:2px;margin-right:8px;'
190
+ f'flex-shrink:0;"></span>'
191
+ f'<span style="color:#e0e0e0;">{label}</span>: '
192
+ f'<span style="color:#aaa;">{counts[label]}x, {total_dur[label]:.1f}s total</span>'
193
+ f'</div>'
194
+ )
195
+ html += "</div>"
196
+ return html