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

Upload data_loader.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. data_loader.py +409 -0
data_loader.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import json
3
+ import os
4
+ import warnings
5
+ from functools import lru_cache
6
+ from typing import Optional
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import s3fs
11
+ from dotenv import load_dotenv
12
+
13
+ load_dotenv(os.path.join(os.path.dirname(__file__), ".env"))
14
+
15
+ warnings.filterwarnings("ignore", category=UserWarning, message=".*asynchronous.*")
16
+
17
+ MANIFEST_DATASET = "tankalapavankalyan/eeg-corpus-manifest"
18
+ PARQUET_BASE = (
19
+ "https://huggingface.co/datasets/tankalapavankalyan/eeg-corpus-manifest"
20
+ "/resolve/refs%2Fconvert%2Fparquet"
21
+ )
22
+
23
+ TUAR_DATASET_IDS = ["tuh_eeg_artifact"]
24
+
25
+ ARTIFACT_LABEL_MAP = {
26
+ "eyem": "Eye Movement",
27
+ "eyeb": "Eye Blink",
28
+ "musc": "Muscle",
29
+ "elec": "Electrode Pop",
30
+ "chew": "Chewing",
31
+ "shiv": "Shiver",
32
+ "null": "Clean",
33
+ "elpp": "Electrode Pop",
34
+ "artf": "Artifact (Generic)",
35
+ "bckg": "Background",
36
+ "eyem_musc": "Eye Movement + Muscle",
37
+ "musc_elec": "Muscle + Electrode Pop",
38
+ "eyem_elec": "Eye Movement + Electrode Pop",
39
+ "eyem_chew": "Eye Movement + Chewing",
40
+ "chew_elec": "Chewing + Electrode Pop",
41
+ "chew_musc": "Chewing + Muscle",
42
+ "eyem_shiv": "Eye Movement + Shiver",
43
+ "shiv_elec": "Shiver + Electrode Pop",
44
+ }
45
+
46
+ ARTIFACT_COLORS = {
47
+ "Eye Movement": "rgba(30, 144, 255, 0.25)",
48
+ "Eye Blink": "rgba(0, 100, 255, 0.25)",
49
+ "Muscle": "rgba(220, 20, 60, 0.25)",
50
+ "Electrode Pop": "rgba(255, 165, 0, 0.25)",
51
+ "Chewing": "rgba(50, 205, 50, 0.25)",
52
+ "Shiver": "rgba(148, 103, 189, 0.25)",
53
+ "Artifact (Generic)": "rgba(128, 128, 128, 0.25)",
54
+ "Background": "rgba(200, 200, 200, 0.08)",
55
+ "Clean": "rgba(200, 200, 200, 0.08)",
56
+ "Eye Movement + Muscle": "rgba(125, 82, 158, 0.25)",
57
+ "Muscle + Electrode Pop": "rgba(238, 93, 30, 0.25)",
58
+ "Eye Movement + Electrode Pop": "rgba(143, 155, 128, 0.25)",
59
+ "Eye Movement + Chewing": "rgba(40, 175, 153, 0.25)",
60
+ "Chewing + Electrode Pop": "rgba(153, 185, 30, 0.25)",
61
+ "Chewing + Muscle": "rgba(135, 113, 56, 0.25)",
62
+ "Eye Movement + Shiver": "rgba(89, 124, 222, 0.25)",
63
+ "Shiver + Electrode Pop": "rgba(202, 134, 95, 0.25)",
64
+ }
65
+
66
+ _fs: Optional[s3fs.S3FileSystem] = None
67
+
68
+
69
+ def get_s3fs() -> s3fs.S3FileSystem:
70
+ global _fs
71
+ if _fs is None:
72
+ key = os.environ.get("AWS_ACCESS_KEY_ID")
73
+ secret = os.environ.get("AWS_SECRET_ACCESS_KEY")
74
+ region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
75
+ if key and secret:
76
+ _fs = s3fs.S3FileSystem(key=key, secret=secret, client_kwargs={"region_name": region})
77
+ else:
78
+ _fs = s3fs.S3FileSystem(anon=True, client_kwargs={"region_name": region})
79
+ return _fs
80
+
81
+
82
+ def reset_s3fs():
83
+ global _fs
84
+ _fs = None
85
+ _zarr_cache.clear()
86
+ _scale_cache.clear()
87
+ _annotation_cache.clear()
88
+
89
+
90
+ MANIFEST_COLUMNS = [
91
+ "recording_id", "dataset_id", "subject_id_in_dataset", "session_id",
92
+ "run_id", "task", "archival_uri", "archival_format", "duration_s",
93
+ "n_channels", "n_eeg_channels", "sampling_rate_hz", "reference",
94
+ "montage_name", "recording_type", "channel_names", "canonical_uri",
95
+ "conversion_status", "roundtrip_class",
96
+ ]
97
+
98
+
99
+ def get_tuar_recordings() -> pd.DataFrame:
100
+ url = f"{PARQUET_BASE}/recordings/train/0000.parquet"
101
+ df = pd.read_parquet(url, columns=MANIFEST_COLUMNS)
102
+ mask = df["dataset_id"].isin(TUAR_DATASET_IDS) & (df["conversion_status"] == "ok")
103
+ result = df[mask].copy()
104
+ del df
105
+ result = result.sort_values("subject_id_in_dataset").reset_index(drop=True)
106
+ return result
107
+
108
+
109
+ def get_recording_display_list(df: pd.DataFrame) -> list[str]:
110
+ entries = []
111
+ for _, row in df.iterrows():
112
+ label = (
113
+ f"{row['recording_id'][:8]}… | "
114
+ f"{row['dataset_id']} | "
115
+ f"subj={row['subject_id_in_dataset']} | "
116
+ f"ses={row.get('session_id', 'N/A')} | "
117
+ f"dur={row['duration_s']:.0f}s | "
118
+ f"{row['n_channels']:.0f}ch @ {row['sampling_rate_hz']:.0f}Hz"
119
+ )
120
+ entries.append(label)
121
+ return entries
122
+
123
+
124
+ # --- Signal access via direct Zarr + S3 ---
125
+
126
+ _zarr_cache: dict[str, object] = {}
127
+ _scale_cache: dict[str, tuple[np.ndarray, np.ndarray]] = {}
128
+
129
+
130
+ def _register_flac_codec():
131
+ """Register the FLAC codec with zarr v3 if not already done."""
132
+ try:
133
+ from zarr.registry import get_codec_class
134
+ get_codec_class("numcodecs.flac")
135
+ except KeyError:
136
+ import numcodecs
137
+ from flac_numcodecs import Flac
138
+ numcodecs.register_codec(Flac)
139
+ from zarr.codecs.numcodecs._codecs import _NumcodecsBytesBytesCodec
140
+ from zarr.codecs import register_codec
141
+
142
+ class FlacCodec(_NumcodecsBytesBytesCodec):
143
+ codec_name = "numcodecs.flac"
144
+ def __init__(self, **kwargs):
145
+ super().__init__(codec_id="flac", codec_config=kwargs)
146
+
147
+ register_codec("numcodecs.flac", FlacCodec)
148
+
149
+
150
+ def _open_zarr(canonical_uri: str):
151
+ if canonical_uri in _zarr_cache:
152
+ return _zarr_cache[canonical_uri]
153
+
154
+ _register_flac_codec()
155
+ fs = get_s3fs()
156
+ s3_path = canonical_uri.replace("s3://", "")
157
+ if not s3_path.endswith("/"):
158
+ s3_path += "/"
159
+
160
+ import zarr
161
+ fsspec_store = zarr.storage.FsspecStore(fs=fs, path=s3_path, read_only=True)
162
+ root = zarr.open_group(fsspec_store, mode="r")
163
+ _zarr_cache[canonical_uri] = root
164
+
165
+ if len(_zarr_cache) > 50:
166
+ oldest = next(iter(_zarr_cache))
167
+ del _zarr_cache[oldest]
168
+ _scale_cache.pop(oldest, None)
169
+
170
+ return root
171
+
172
+
173
+ def _get_scale_offset(canonical_uri: str):
174
+ if canonical_uri in _scale_cache:
175
+ return _scale_cache[canonical_uri]
176
+
177
+ root = _open_zarr(canonical_uri)
178
+ ch_grp = root["channels"]
179
+ phys_min = np.array(ch_grp["physical_min"][:], dtype=np.float64)
180
+ phys_max = np.array(ch_grp["physical_max"][:], dtype=np.float64)
181
+ dig_min = np.array(ch_grp["digital_min"][:], dtype=np.float64)
182
+ dig_max = np.array(ch_grp["digital_max"][:], dtype=np.float64)
183
+
184
+ scale = (phys_max - phys_min) / (dig_max - dig_min + 1e-12)
185
+ offset = phys_min - dig_min * scale
186
+ _scale_cache[canonical_uri] = (scale.astype(np.float32), offset.astype(np.float32))
187
+ return _scale_cache[canonical_uri]
188
+
189
+
190
+ def read_signal_window(
191
+ canonical_uri: str,
192
+ start_sample: int,
193
+ end_sample: int,
194
+ channel_indices: Optional[list[int]] = None,
195
+ ) -> np.ndarray:
196
+ root = _open_zarr(canonical_uri)
197
+ sig_arr = root["signal"]
198
+
199
+ if channel_indices is not None:
200
+ raw = np.array(sig_arr[channel_indices, start_sample:end_sample], dtype=np.float32)
201
+ else:
202
+ raw = np.array(sig_arr[:, start_sample:end_sample], dtype=np.float32)
203
+
204
+ scale, offset = _get_scale_offset(canonical_uri)
205
+ if channel_indices is not None:
206
+ scale = scale[channel_indices]
207
+ offset = offset[channel_indices]
208
+
209
+ data = raw * scale[:, None] + offset[:, None]
210
+
211
+ kernel_size = 5
212
+ if data.shape[1] > kernel_size:
213
+ kernel = np.ones(kernel_size) / kernel_size
214
+ for i in range(data.shape[0]):
215
+ data[i] = np.convolve(data[i], kernel, mode="same")
216
+
217
+ return data
218
+
219
+
220
+ def get_channel_names(canonical_uri: str) -> list[str]:
221
+ root = _open_zarr(canonical_uri)
222
+ return list(root["channels"]["name"][:])
223
+
224
+
225
+ def get_store_metadata(canonical_uri: str) -> dict:
226
+ root = _open_zarr(canonical_uri)
227
+ attrs = dict(root.attrs)
228
+ return {
229
+ "n_channels": root["signal"].shape[0],
230
+ "n_samples": root["signal"].shape[1],
231
+ "sampling_rate_hz": attrs.get("sampling_rate_hz"),
232
+ "duration_s": attrs.get("duration_s"),
233
+ "channel_names": list(root["channels"]["name"][:]),
234
+ "reference": attrs.get("reference"),
235
+ "montage_name": attrs.get("montage_name"),
236
+ "recording_type": attrs.get("recording_type"),
237
+ "manufacturer": attrs.get("manufacturer"),
238
+ "source_uri": attrs.get("source_uri"),
239
+ }
240
+
241
+
242
+ # --- TUAR artifact annotations from CSV companion files ---
243
+
244
+ _annotation_cache: dict[str, list[dict]] = {}
245
+
246
+
247
+ def _get_csv_path_from_source_uri(source_uri: str) -> Optional[str]:
248
+ if not source_uri or not source_uri.endswith(".edf"):
249
+ return None
250
+ return source_uri.replace("s3://", "").rsplit(".", 1)[0] + ".csv"
251
+
252
+
253
+ def get_annotations(canonical_uri: str, source_uri: Optional[str] = None) -> list[dict]:
254
+ cache_key = canonical_uri
255
+ if cache_key in _annotation_cache:
256
+ return _annotation_cache[cache_key]
257
+
258
+ if source_uri is None:
259
+ try:
260
+ rec = _open_recording(canonical_uri)
261
+ source_uri = rec.metadata.source_uri
262
+ except Exception:
263
+ _annotation_cache[cache_key] = []
264
+ return []
265
+
266
+ csv_path = _get_csv_path_from_source_uri(source_uri)
267
+ if csv_path is None:
268
+ _annotation_cache[cache_key] = []
269
+ return []
270
+
271
+ try:
272
+ fs = get_s3fs()
273
+ raw = fs.cat(csv_path).decode("utf-8")
274
+ except Exception:
275
+ _annotation_cache[cache_key] = []
276
+ return []
277
+
278
+ annotations = _parse_tuar_csv(raw)
279
+ _annotation_cache[cache_key] = annotations
280
+ return annotations
281
+
282
+
283
+ def preload_all_annotations(df: pd.DataFrame) -> None:
284
+ """Bulk-fetch all CSV annotation files in one batch S3 call."""
285
+ paths_map: dict[str, str] = {}
286
+
287
+ for _, row in df.iterrows():
288
+ canonical_uri = row.get("canonical_uri", "")
289
+ archival_uri = row.get("archival_uri", "")
290
+ if not canonical_uri or canonical_uri in _annotation_cache:
291
+ continue
292
+ csv_path = _get_csv_path_from_source_uri(archival_uri)
293
+ if csv_path:
294
+ paths_map[csv_path] = canonical_uri
295
+
296
+ if not paths_map:
297
+ return
298
+
299
+ fs = get_s3fs()
300
+ csv_paths = list(paths_map.keys())
301
+
302
+ BATCH = 50
303
+ for i in range(0, len(csv_paths), BATCH):
304
+ batch = csv_paths[i:i + BATCH]
305
+ try:
306
+ results = fs.cat(batch, on_error="return")
307
+ except Exception:
308
+ continue
309
+
310
+ if isinstance(results, dict):
311
+ for csv_path, content in results.items():
312
+ canonical_uri = paths_map[csv_path]
313
+ if isinstance(content, bytes):
314
+ try:
315
+ _annotation_cache[canonical_uri] = _parse_tuar_csv(content.decode("utf-8"))
316
+ except Exception:
317
+ _annotation_cache[canonical_uri] = []
318
+ else:
319
+ _annotation_cache[canonical_uri] = []
320
+
321
+
322
+ def _parse_tuar_csv(raw: str) -> list[dict]:
323
+ lines = [l for l in raw.strip().split("\n") if not l.startswith("#") and l.strip()]
324
+ if not lines:
325
+ return []
326
+
327
+ header_line = lines[0]
328
+ if "channel" in header_line and "start_time" in header_line:
329
+ lines = lines[1:]
330
+
331
+ seen = set()
332
+ annotations = []
333
+
334
+ for line in lines:
335
+ parts = line.strip().split(",")
336
+ if len(parts) < 4:
337
+ continue
338
+
339
+ channel = parts[0].strip()
340
+ try:
341
+ start = float(parts[1].strip())
342
+ stop = float(parts[2].strip())
343
+ except ValueError:
344
+ continue
345
+ raw_label = parts[3].strip().lower()
346
+ confidence = float(parts[4].strip()) if len(parts) > 4 else 1.0
347
+
348
+ dedup_key = (round(start, 3), round(stop, 3), raw_label)
349
+ if dedup_key in seen:
350
+ continue
351
+ seen.add(dedup_key)
352
+
353
+ label = ARTIFACT_LABEL_MAP.get(raw_label, raw_label.title())
354
+ color = ARTIFACT_COLORS.get(label, "rgba(128, 128, 128, 0.2)")
355
+
356
+ annotations.append({
357
+ "onset_s": start,
358
+ "duration_s": stop - start,
359
+ "end_s": stop,
360
+ "raw_label": raw_label,
361
+ "label": label,
362
+ "color": color,
363
+ "channel": channel,
364
+ "confidence": confidence,
365
+ })
366
+
367
+ annotations.sort(key=lambda a: a["onset_s"])
368
+ return annotations
369
+
370
+
371
+ def get_annotations_in_window(
372
+ canonical_uri: str,
373
+ start_s: float,
374
+ end_s: float,
375
+ source_uri: Optional[str] = None,
376
+ ) -> list[dict]:
377
+ all_ann = get_annotations(canonical_uri, source_uri)
378
+ return [a for a in all_ann if a["end_s"] > start_s and a["onset_s"] < end_s]
379
+
380
+
381
+ def get_recording_info(row: pd.Series) -> dict:
382
+ channel_names = row.get("channel_names", [])
383
+ if isinstance(channel_names, str):
384
+ try:
385
+ channel_names = json.loads(channel_names)
386
+ except (json.JSONDecodeError, TypeError):
387
+ channel_names = []
388
+ if not isinstance(channel_names, list):
389
+ channel_names = list(channel_names)
390
+
391
+ return {
392
+ "recording_id": row["recording_id"],
393
+ "dataset_id": row["dataset_id"],
394
+ "subject": row.get("subject_id_in_dataset", "N/A"),
395
+ "session": row.get("session_id", "N/A"),
396
+ "task": row.get("task", "N/A"),
397
+ "duration_s": row.get("duration_s", 0),
398
+ "n_channels": row.get("n_channels", 0),
399
+ "n_eeg_channels": row.get("n_eeg_channels", 0),
400
+ "sampling_rate_hz": row.get("sampling_rate_hz", 0),
401
+ "reference": row.get("reference", "N/A"),
402
+ "montage_name": row.get("montage_name", "N/A"),
403
+ "recording_type": row.get("recording_type", "N/A"),
404
+ "archival_format": row.get("archival_format", "N/A"),
405
+ "canonical_uri": row.get("canonical_uri", ""),
406
+ "archival_uri": row.get("archival_uri", ""),
407
+ "channel_names": channel_names,
408
+ "roundtrip_class": row.get("roundtrip_class", "N/A"),
409
+ }