11#OPENBCI HEX TO CSV CONVERSION REFERENCE: https://github.com/roflecopter/openbci-psg
2- # GIVES output same as OpenBCI GUI 4.2
32
43import numpy as np
54import pandas as pd
@@ -82,20 +81,20 @@ def processLine(split_line):
8281 return values_array
8382
8483
85-
86-
87- def process_file (file_path :str ,
88- n_ch :int = 16 ,
89- n_acc :int = 3 ,
90- save_path :str = None ):
84+ def process_file (file_path : str ,
85+ n_acc : int = 3 ,
86+ save_path : str = None ,
87+ board_type : str = "CytonDaisy" ,
88+ board_mode : str = "Analog" ):
9189 """
9290 Process the OpenBCI hex file and convert it to a CSV file.
9391
9492 Parameters:
9593 - file_path (str): Path to the input hex file.
96- - n_ch (int): Number of EEG channels.
9794 - n_acc (int): Number of accelerometer channels.
9895 - save_path (str): Path to save the converted CSV file.
96+ - board_type (str): Type of OpenBCI board used (CytonDaisy, Cyton, Ganglion).
97+ - board_mode (str): Mode of the board data (Analog, Digital, Mixed).
9998
10099 Returns:
101100 - None
@@ -104,6 +103,17 @@ def process_file(file_path:str,
104103 current_time = datetime .now ()
105104 formatted_time = current_time .strftime ("%Y-%m-%d_%H-%M-%S" )
106105
106+ # Determine the number of channels based on the board type
107+ if board_type == "CytonDaisy" :
108+ n_ch = 16
109+ board_name = "OpenBCI_GUI$BoardCytonDaisySerial"
110+ elif board_type == "Cyton" :
111+ n_ch = 8
112+ board_name = "OpenBCI_GUI$BoardCytonSerial"
113+ elif board_type == "Ganglion" :
114+ n_ch = 4
115+ board_name = "OpenBCI_GUI$BoardGanglionSerial"
116+
107117 # Open the hex file for reading
108118 with open (file_path , 'r' ) as file :
109119 result = [] # List to store parsed values
@@ -146,54 +156,80 @@ def process_file(file_path:str,
146156 bci_signals = np .array (result )
147157
148158 # Apply OpenBCI scaling to EEG signals and accelerometer scaling to accelerometer signals
149- signals_V = np .vectorize (adc_v_bci )(bci_signals [:, :16 ])
150- accel_data = np .vectorize (accel_scale )(bci_signals [:, 16 :])
159+ accel_data = np .vectorize (accel_scale )(bci_signals [:, n_ch :])
151160
152- # Concatenate EEG and accelerometer data along the columns
153- data = np .concatenate ((signals_V , accel_data ), axis = 1 )
161+ exg_channel_cols = [f'EXG Channel { i } ' for i in range (n_ch )]
162+
163+ additional_cols = ['Accel Channel 0' , 'Accel Channel 1' , 'Accel Channel 2' ,
164+ 'Not Used' , 'Digital Channel 0 (D11)' , 'Digital Channel 1 (D12)' ,
165+ 'Digital Channel 2 (D13)' , 'Digital Channel 3 (D17)' , 'Not Used' ,
166+ 'Digital Channel 4 (D18)' , 'Analog Channel 0' , 'Analog Channel 1' ,
167+ 'Analog Channel 2' , 'Timestamp' , 'Marker Channel' , 'Timestamp (Formatted)' ]
168+
169+ all_columns = ['Sample Index' ] + exg_channel_cols + additional_cols
170+
171+ # create columns
172+ df = pd .DataFrame (columns = all_columns )
173+
174+ # sample index values
175+ num_repeats = len (bci_signals ) // 256
176+ df ['Sample Index' ] = list (range (256 )) * num_repeats + list (range (len (bci_signals ) % 256 ))
177+
178+ # EXG channels values
179+ for i in range (n_ch ):
180+ df [exg_channel_cols [i ]] = np .vectorize (adc_v_bci )(bci_signals [:, i ])
181+
182+ # Sensor values
183+ analog_sensor_columns = [- 6 , - 5 , - 4 ]
184+ digital_sensor_columns = [- 11 , - 10 , - 7 ]
185+ accel_sensor_columns = [- 16 , - 15 , - 14 ]
186+
187+ # Map data to correct columns based on board_mode
188+ if board_mode == "Analog" :
189+ df .iloc [:, analog_sensor_columns ] = accel_data
190+ elif board_mode == "Digital" :
191+ df .iloc [:, digital_sensor_columns ] = accel_data
192+ else :
193+ df .iloc [:, accel_sensor_columns ] = accel_data
154194
155- # Generate an index for the output CSV file
156- index = [str (i ) for i in range (1 , len (data ) + 1 )]
195+ df = df .fillna (0 )
157196
158197 # Additional information to be added at the beginning of the CSV file
159198 additional_info = [
160- f"%OBCI SD Convert - { formatted_time } " ,
161- "%" ,
162- "%Sample Rate = 250.0 Hz" ,
163- "%First Column = SampleIndex" ,
164- "%Last Column = Timestamp" ,
165- "%Other Columns = EEG data in microvolts followed by Accel Data (in G) interleaved with Aux Data" ,
199+ "%OpenBCI Raw EXG Data" ,
200+ f"%Number of channels = { n_ch } " ,
201+ "%Sample Rate = 250 Hz" ,
202+ f"%Board = { board_name } "
166203 ]
167204
168- # Create a Pandas DataFrame from the data and save it to a CSV file
169- make_csv = pd .DataFrame (data , index = index )
170205 file_name_with_extension = os .path .basename (file_path )
171- file_name , file_extension = os .path .splitext (file_name_with_extension )
206+ file_name , _ = os .path .splitext (file_name_with_extension )
172207
173208 if save_path :
174209 file_path = os .path .join (save_path , file_name )
175- make_csv .to_csv (f"{ file_path } _converted.csv" )
210+ df .to_csv (f"{ file_path } _converted.csv" , index = False )
176211
177212 # Write additional information to the CSV file
178213 with open (f"{ file_path } _converted.csv" , 'w' ) as file :
179214 for line in additional_info :
180215 file .write (line + '\n ' )
181216
182217 # Append the index to the CSV file
183- make_csv .to_csv (f"{ file_path } _converted.csv" , mode = 'a' , index = index )
218+ df .to_csv (f"{ file_path } _converted.csv" , mode = 'a' , index = False )
184219 else :
185- make_csv .to_csv (f"./{ file_name } _converted.csv" )
220+ df .to_csv (f"./{ file_name } _converted.csv" , index = False )
186221
187222 # Write additional information to the CSV file
188223 with open (f"{ file_name } _converted.csv" , 'w' ) as file :
189224 for line in additional_info :
190225 file .write (line + '\n ' )
191226
192227 # Append the index to the CSV file
193- make_csv .to_csv (f"{ file_path } _converted.csv" , mode = 'a' , index = index )
228+ df .to_csv (f"{ file_path } _converted.csv" , mode = 'a' , index = False )
194229
195230 return None
196231
232+
197233
198234def adc_v_bci (signal ,
199235 ADS1299_VREF :float = 4.5 ,
@@ -229,8 +265,8 @@ def adc_v_bci(signal,
229265
230266def start_converting (sd_dir :str = "./" ,
231267 gain :int = 24 ,
232- n_ch : int = 16 ,
233- n_acc : int = 3 ,
268+ board_type : str = "CytonDaisy" ,
269+ board_mode : str = "Analog" ,
234270 save_path :str = "" ):
235271 """
236272 Batch process OpenBCI hex files in a directory and convert them to CSV.
@@ -247,8 +283,6 @@ def start_converting(sd_dir:str = "./",
247283 """
248284 # Assign parameters to local variables
249285 gain = gain
250- n_ch = n_ch
251- n_acc = n_acc
252286 save_path = save_path
253287
254288 # Get a list of files in the specified directory with a '.txt' extension
@@ -267,15 +301,18 @@ def start_converting(sd_dir:str = "./",
267301 print (f'converting: { file_name } ' )
268302
269303 # Call the process_file function to convert the hex file to CSV
270- process_file (file_path = file_path , save_path = save_path )
304+ process_file (file_path = file_path ,
305+ save_path = save_path ,
306+ board_type = board_type ,
307+ board_mode = board_mode )
271308
272309 return None
273310
274311
275312
276313def file_type_conversion (csv_path :str = "./" ,
277314 save_path :str = "./" ,
278- num_channels : int = 16 ,
315+ board_type : str = "CytonDaisy" ,
279316 ch_names :[str ] = None ,
280317 sfreq :int = 250 ,
281318 file_type :str = "brainvision" ):
@@ -293,6 +330,16 @@ def file_type_conversion(csv_path:str = "./",
293330 Returns:
294331 - None
295332 """
333+
334+ if board_type == "CytonDaisy" :
335+ n_ch = 16
336+
337+ elif board_type == "Cyton" :
338+ n_ch = 8
339+
340+ elif board_type == "Ganglion" :
341+ n_ch = 4
342+
296343
297344 # Get a list of files in the specified directory with a '.csv' extension
298345 files = [file for file in os .listdir (csv_path ) if file .endswith ('.csv' )]
@@ -305,12 +352,13 @@ def file_type_conversion(csv_path:str = "./",
305352 for file_name in files :
306353 # Construct the full path to the CSV file
307354 file_path = os .path .join (csv_path , file_name )
355+ name , _ = os .path .splitext (file_name )
308356
309357 # Read the CSV file, skipping the header rows
310- csv_file = pd .read_csv (file_path ,header = None ,skiprows = [0 ,1 ,2 ,3 ,4 , 5 , 6 ],index_col = 0 ,sep = ',' ,engine = 'python' )
358+ csv_file = pd .read_csv (file_path ,header = None ,skiprows = [0 ,1 ,2 ,3 ,4 ],index_col = 0 ,sep = ',' ,engine = 'python' )
311359
312360 # Remove the last 3 columns from the CSV file
313- csv_file = csv_file .iloc [:,:- 3 ]
361+ csv_file = csv_file .iloc [:,:- 16 ]
314362
315363 # Transpose the DataFrame
316364 csv_file = pd .DataFrame .transpose (csv_file )
@@ -319,29 +367,39 @@ def file_type_conversion(csv_path:str = "./",
319367 csv_file = csv_file / 1e6
320368
321369 # Define EEG channel types
322- ch_types = (['eeg' ] * num_channels )
370+ ch_types = (['eeg' ] * n_ch )
323371
324372 # If channel names are not provided, generate default names
325373 if ch_names is None :
326- ch_names = [str (i ) for i in range (1 , num_channels + 1 )]
374+ ch_names = [str (i ) for i in range (1 , n_ch + 1 )]
327375
328376 # Create MNE Info object
329377 info = mne .create_info (ch_names = ch_names , sfreq = sfreq , ch_types = ch_types )
330378
331379 # Create MNE RawArray
332380 raw = mne .io .RawArray (csv_file , info )
333381
382+ #getting file extension
383+ if file_type == "brainvision" :
384+ extension = ".eeg"
385+ elif file_type == "edf" :
386+ extension = ".edf"
387+ elif file_type == "EEGLAB" :
388+ extension = ".set"
389+
334390 # Export the MNE RawArray to the specified file type
335- mne .export .export_raw (f"{ save_path } /{ file_name } .eeg " , raw , fmt = file_type , overwrite = True )
391+ mne .export .export_raw (f"{ save_path } /{ name } { extension } " , raw , fmt = file_type , overwrite = True )
336392
337393 return None
338394
339395if __name__ == "__main__" :
340396 start_converting (sd_dir = "./hex_data/" ,
341- save_path = "raw_data" )
397+ save_path = "raw_data" ,
398+ board_type = "CytonDaisy" ,
399+ board_mode = "Analog" )
342400
343401 file_type_conversion (csv_path = "./raw_data/" ,
344402 save_path = "./raw_data/" ,
345- num_channels = 16 ,
403+ board_type = "CytonDaisy" ,
346404 sfreq = 250 ,
347405 file_type = "brainvision" )
0 commit comments