v2.0.0
Loading...
Searching...
No Matches
fiff_annotations.cpp
Go to the documentation of this file.
1//=============================================================================================================
16
17//=============================================================================================================
18// INCLUDES
19//=============================================================================================================
20
21#include "fiff_annotations.h"
22
23//=============================================================================================================
24// QT INCLUDES
25//=============================================================================================================
26
27#include <QFile>
28#include <QJsonDocument>
29#include <QJsonObject>
30#include <QJsonArray>
31#include <QTextStream>
32#include <QDebug>
33#include <QFileInfo>
34
35//=============================================================================================================
36// USED NAMESPACES
37//=============================================================================================================
38
39using namespace FIFFLIB;
40
41//=============================================================================================================
42// DEFINE MEMBER METHODS
43//=============================================================================================================
44
48
49//=============================================================================================================
50
52{
53 return m_annotations.size();
54}
55
56//=============================================================================================================
57
59{
60 return m_annotations.isEmpty();
61}
62
63//=============================================================================================================
64
66{
67 return m_annotations[index];
68}
69
70//=============================================================================================================
71
73{
74 return m_annotations[index];
75}
76
77//=============================================================================================================
78
80{
81 m_annotations.append(annotation);
82}
83
84//=============================================================================================================
85
86void FiffAnnotations::append(double onset, double duration, const QString& description,
87 const QStringList& channelNames, const QString& comment)
88{
90 a.onset = onset;
91 a.duration = duration;
92 a.description = description;
93 a.channelNames = channelNames;
94 a.comment = comment;
95 m_annotations.append(a);
96}
97
98//=============================================================================================================
99
101{
102 m_annotations.remove(index);
103}
104
105//=============================================================================================================
106
108{
109 m_annotations.clear();
110}
111
112//=============================================================================================================
113
114const QVector<FiffAnnotation>& FiffAnnotations::toVector() const
115{
116 return m_annotations;
117}
118
119//=============================================================================================================
120
121int FiffAnnotations::onsetToSample(int index, double sfreq, int firstSample) const
122{
123 return static_cast<int>(m_annotations[index].onset * sfreq) + firstSample;
124}
125
126//=============================================================================================================
127
128int FiffAnnotations::endToSample(int index, double sfreq, int firstSample) const
129{
130 const FiffAnnotation& a = m_annotations[index];
131 return static_cast<int>((a.onset + a.duration) * sfreq) + firstSample;
132}
133
134//=============================================================================================================
135
136FiffAnnotations FiffAnnotations::select(const QString& descriptionFilter) const
137{
138 FiffAnnotations result;
139 for (const FiffAnnotation& a : m_annotations) {
140 if (a.description.startsWith(descriptionFilter)) {
141 result.append(a);
142 }
143 }
144 return result;
145}
146
147//=============================================================================================================
148
149FiffAnnotations FiffAnnotations::selectByChannel(const QString& channelName) const
150{
151 FiffAnnotations result;
152 for (const FiffAnnotation& a : m_annotations) {
153 if (a.channelNames.isEmpty() || a.channelNames.contains(channelName)) {
154 result.append(a);
155 }
156 }
157 return result;
158}
159
160//=============================================================================================================
161
162FiffAnnotations FiffAnnotations::crop(double tmin, double tmax) const
163{
164 FiffAnnotations result;
165 for (const FiffAnnotation& a : m_annotations) {
166 double aEnd = a.onset + a.duration;
167
168 // Check overlap: annotation interval [onset, aEnd] vs window [tmin, tmax]
169 if (a.onset > tmax || aEnd < tmin) {
170 continue;
171 }
172
173 FiffAnnotation clipped = a;
174 if (clipped.onset < tmin) {
175 double shift = tmin - clipped.onset;
176 clipped.onset = tmin;
177 clipped.duration -= shift;
178 }
179 double clippedEnd = clipped.onset + clipped.duration;
180 if (clippedEnd > tmax) {
181 clipped.duration = tmax - clipped.onset;
182 }
183 result.append(clipped);
184 }
185 return result;
186}
187
188//=============================================================================================================
189
191{
192 FiffAnnotations annot;
193
194 QFile file(path);
195 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
196 qWarning() << "[FiffAnnotations::readJson] Cannot open file:" << path;
197 return annot;
198 }
199
200 QByteArray data = file.readAll();
201 file.close();
202
203 QJsonParseError parseError;
204 QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
205 if (parseError.error != QJsonParseError::NoError) {
206 qWarning() << "[FiffAnnotations::readJson] JSON parse error:" << parseError.errorString();
207 return annot;
208 }
209
210 QJsonObject root = doc.object();
211 QJsonArray arr = root.value("annotations").toArray();
212
213 for (int i = 0; i < arr.size(); ++i) {
214 QJsonObject obj = arr[i].toObject();
216 a.onset = obj.value("onset").toDouble(0.0);
217 a.duration = obj.value("duration").toDouble(0.0);
218 a.description = obj.value("description").toString();
219
220 QJsonArray chArr = obj.value("channel_names").toArray();
221 for (int j = 0; j < chArr.size(); ++j) {
222 a.channelNames.append(chArr[j].toString());
223 }
224
225 a.comment = obj.value("comment").toString();
226 a.extras = obj.value("extras").toObject().toVariantMap();
227
228 annot.append(a);
229 }
230
231 return annot;
232}
233
234//=============================================================================================================
235
236bool FiffAnnotations::writeJson(const QString& path, const FiffAnnotations& annot)
237{
238 QJsonArray arr;
239 for (const FiffAnnotation& a : annot.toVector()) {
240 QJsonObject obj;
241 obj["onset"] = a.onset;
242 obj["duration"] = a.duration;
243 obj["description"] = a.description;
244
245 QJsonArray chArr;
246 for (const QString& ch : a.channelNames) {
247 chArr.append(ch);
248 }
249 obj["channel_names"] = chArr;
250 obj["comment"] = a.comment;
251 obj["extras"] = QJsonObject::fromVariantMap(a.extras);
252 arr.append(obj);
253 }
254
255 QJsonObject root;
256 root["annotations"] = arr;
257 QJsonDocument doc(root);
258
259 QFile file(path);
260 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
261 qWarning() << "[FiffAnnotations::writeJson] Cannot open file for writing:" << path;
262 return false;
263 }
264 file.write(doc.toJson(QJsonDocument::Indented));
265 file.close();
266 return true;
267}
268
269//=============================================================================================================
270
272{
273 FiffAnnotations annot;
274
275 QFile file(path);
276 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
277 qWarning() << "[FiffAnnotations::readCsv] Cannot open file:" << path;
278 return annot;
279 }
280
281 QTextStream in(&file);
282 // Skip header line
283 if (!in.atEnd()) {
284 in.readLine();
285 }
286
287 while (!in.atEnd()) {
288 QString line = in.readLine().trimmed();
289 if (line.isEmpty()) {
290 continue;
291 }
292
293 // Parse: onset,duration,description
294 // Description may contain commas, so split only the first two commas
295 int firstComma = line.indexOf(',');
296 if (firstComma < 0) continue;
297 int secondComma = line.indexOf(',', firstComma + 1);
298 if (secondComma < 0) continue;
299
300 bool onsetOk = false, durationOk = false;
301 double onset = line.left(firstComma).toDouble(&onsetOk);
302 double duration = line.mid(firstComma + 1, secondComma - firstComma - 1).toDouble(&durationOk);
303 QString description = line.mid(secondComma + 1);
304
305 if (!onsetOk || !durationOk) {
306 continue;
307 }
308
309 annot.append(onset, duration, description);
310 }
311
312 file.close();
313 return annot;
314}
315
316//=============================================================================================================
317
318bool FiffAnnotations::writeCsv(const QString& path, const FiffAnnotations& annot)
319{
320 QFile file(path);
321 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
322 qWarning() << "[FiffAnnotations::writeCsv] Cannot open file for writing:" << path;
323 return false;
324 }
325
326 QTextStream out(&file);
327 out << "onset,duration,description\n";
328
329 for (const FiffAnnotation& a : annot.toVector()) {
330 out << a.onset << "," << a.duration << "," << a.description << "\n";
331 }
332
333 file.close();
334 return true;
335}
336
337//=============================================================================================================
338
340{
341 QString ext = QFileInfo(path).suffix().toLower();
342
343 if (ext == "json") {
344 return readJson(path);
345 } else if (ext == "csv") {
346 return readCsv(path);
347 }
348
349 // Unknown extension: try JSON first, then CSV
350 FiffAnnotations annot = readJson(path);
351 if (!annot.isEmpty()) {
352 return annot;
353 }
354 return readCsv(path);
355}
356
357//=============================================================================================================
358
359bool FiffAnnotations::write(const QString& path, const FiffAnnotations& annot)
360{
361 QString ext = QFileInfo(path).suffix().toLower();
362
363 if (ext == "csv") {
364 return writeCsv(path, annot);
365 }
366 // Default to JSON
367 return writeJson(path, annot);
368}
FIFF / MNE annotations: time-tagged textual marks (BAD_*, EDGE, custom) with onset,...
FIFF file I/O, in-memory data structures and high-level readers/writers.
One FIFF / MNE annotation: onset (s), duration (s) and description string.
int onsetToSample(int index, double sfreq, int firstSample=0) const
static FiffAnnotations readCsv(const QString &path)
const QVector< FiffAnnotation > & toVector() const
FiffAnnotations selectByChannel(const QString &channelName) const
FiffAnnotations select(const QString &descriptionFilter) const
static FiffAnnotations read(const QString &path)
static bool write(const QString &path, const FiffAnnotations &annot)
static bool writeJson(const QString &path, const FiffAnnotations &annot)
int endToSample(int index, double sfreq, int firstSample=0) const
static bool writeCsv(const QString &path, const FiffAnnotations &annot)
static FiffAnnotations readJson(const QString &path)
void append(const FiffAnnotation &annotation)
FiffAnnotations crop(double tmin, double tmax) const
const FiffAnnotation & operator[](int index) const