diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..174c4c3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +# ignoring this might speed up build +# by preventing passing extra content to the docker daemon + + +# General +.DS_Store + +.github +.vscode + +**/data +**/docs +**/tests +**/*.zip +**/*.asc +**/*.edf +**/*.EDF + +**/build +.pytest_cache +.mypy_cache +coverage_html +.coverage diff --git a/.flake8 b/.flake8 index cfd079e..c7f4f5e 100644 --- a/.flake8 +++ b/.flake8 @@ -11,6 +11,8 @@ docstring-convention = numpy max-line-length = 90 max_complexity = 15 max_function_length = 100 +per-file-ignores = + **/__init__.py: D104 # for compatibility with black # https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 extend-ignore = E203 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..a7c19b7 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,23 @@ +--- +name: docker + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Build docker image + run: docker build -t eye2bids:latest . diff --git a/.gitignore b/.gitignore index f061a92..11bd6ca 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ tests/data/test_data.zip **/*.asc **/*.edf **/*.EDF +eye2bids/_version.py + +# General +.DS_Store # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..15cbd13 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,110 @@ +# Generated by Neurodocker and Reproenv. + +FROM ubuntu:22.04 +RUN apt-get update -qq \ + && apt-get install -y -q --no-install-recommends \ + ca-certificates \ + curl \ + gcc \ + gnupg2 \ + pip \ + python3 \ + software-properties-common \ + && rm -rf /var/lib/apt/lists/* +RUN apt-key adv --fetch-keys https://apt.sr-research.com/SRResearch_key && add-apt-repository 'deb [arch=amd64] https://apt.sr-research.com SRResearch main' +RUN apt-get update -qq \ + && apt-get install -y -q --no-install-recommends \ + eyelink-display-software \ + && rm -rf /var/lib/apt/lists/* +RUN mkdir /eye2bids +COPY [".", \ + "/eye2bids"] +WORKDIR /eye2bids +RUN pip install .[dev] + +# Save specification to JSON. +RUN printf '{ \ + "pkg_manager": "apt", \ + "existing_users": [ \ + "root" \ + ], \ + "instructions": [ \ + { \ + "name": "from_", \ + "kwds": { \ + "base_image": "ubuntu:22.04" \ + } \ + }, \ + { \ + "name": "install", \ + "kwds": { \ + "pkgs": [ \ + "gnupg2", \ + "curl", \ + "gcc", \ + "ca-certificates", \ + "software-properties-common", \ + "python3", \ + "pip" \ + ], \ + "opts": null \ + } \ + }, \ + { \ + "name": "run", \ + "kwds": { \ + "command": "apt-get update -qq \\\\\\n && apt-get install -y -q --no-install-recommends \\\\\\n ca-certificates \\\\\\n curl \\\\\\n gcc \\\\\\n gnupg2 \\\\\\n pip \\\\\\n python3 \\\\\\n software-properties-common \\\\\\n && rm -rf /var/lib/apt/lists/*" \ + } \ + }, \ + { \ + "name": "run", \ + "kwds": { \ + "command": "apt-key adv --fetch-keys https://apt.sr-research.com/SRResearch_key && add-apt-repository '"'"'deb [arch=amd64] https://apt.sr-research.com SRResearch main'"'"'" \ + } \ + }, \ + { \ + "name": "install", \ + "kwds": { \ + "pkgs": [ \ + "eyelink-display-software" \ + ], \ + "opts": null \ + } \ + }, \ + { \ + "name": "run", \ + "kwds": { \ + "command": "apt-get update -qq \\\\\\n && apt-get install -y -q --no-install-recommends \\\\\\n eyelink-display-software \\\\\\n && rm -rf /var/lib/apt/lists/*" \ + } \ + }, \ + { \ + "name": "run", \ + "kwds": { \ + "command": "mkdir /eye2bids" \ + } \ + }, \ + { \ + "name": "copy", \ + "kwds": { \ + "source": [ \ + ".", \ + "/eye2bids" \ + ], \ + "destination": "/eye2bids" \ + } \ + }, \ + { \ + "name": "workdir", \ + "kwds": { \ + "path": "/eye2bids" \ + } \ + }, \ + { \ + "name": "run", \ + "kwds": { \ + "command": "pip install .[dev]" \ + } \ + } \ + ] \ +}' > /.reproenv.json +# End saving to specification to JSON. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ed62692 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY: Dockerfile + +Dockerfile: + docker run --rm repronim/neurodocker:0.9.5 generate docker \ + --pkg-manager apt \ + --base-image ubuntu:22.04 \ + --install gnupg2 curl gcc ca-certificates software-properties-common python3 pip \ + --run "apt-key adv --fetch-keys https://apt.sr-research.com/SRResearch_key && add-apt-repository 'deb [arch=amd64] https://apt.sr-research.com SRResearch main'" \ + --install eyelink-display-software \ + --run "mkdir /eye2bids" \ + --copy "." "/eye2bids" \ + --workdir "/eye2bids" \ + --run "pip install .[dev]" \ + > Dockerfile + +docker_build: + docker build -t eye2bids . + +test_data: + python tools/download_test_data.py diff --git a/README.md b/README.md index 8df0941..517e573 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,80 @@ # eye2bids -## Instructions for testing conversion_json +## Installation ### Requirements -- edf file by EyeLink Eye Tracker -- Python3 -- EyeLink Developers Kit. Download from [SR-Research support forum] (forum registration required) +- Python >= 3.8 + +If you want to use eye2bids to convert EyeLink data, +you will need to install the EyeLink Developers Kit. +It can be downloaded from SR-Research support forum (forum registration required). + +The installation on Ubuntu can also be done with the following commands: + +Taken from: https://www.sr-research.com/support/docs.php?topic=linuxsoftware + +```bash +sudo add-apt-repository universe +sudo apt update +sudo apt install ca-certificates +sudo apt-key adv --fetch-keys https://apt.sr-research.com/SRResearch_key +sudo add-apt-repository 'deb [arch=amd64] https://apt.sr-research.com SRResearch main' +sudo apt update +sudo apt install eyelink-display-software +``` + +### Install eye2bids + +- Clone the repository + +```bash +git clone https://github.com/bids-standard/eye2bids.git +``` + +- Install the package in editatble mode + +```bash +cd eye2bids +pip install . +``` + +## Using eye2bids + +- Supporeted Input data: + + - edf file by EyeLink Eye Tracker + +To try it, you can install our test data from OSF by running the following command: + +```bash +python tools/download_test_data.py +``` + - manual_metadata.yml file (find template and an example in conversion_json folder) ### Run code -``` -python3 edf2bids_json.py +```bash +python edf2bids_json.py ``` [SR-Research support forum]: https://www.sr-research.com/support/forum-9.html + +## Docker + +You can build the docker image with the following command: + +```bash +docker build -t eye2bids:latest . +``` + +## Contributing + +Make sure you install eye2bids in editable mode (see above) and install the development dependencies: + +```bash +pip install --editable .[dev] +``` + +## Related projects diff --git a/conversion_json/edf2bids_json.py b/conversion_json/edf2bids_json.py deleted file mode 100644 index cfc7086..0000000 --- a/conversion_json/edf2bids_json.py +++ /dev/null @@ -1,244 +0,0 @@ -"""Main module for conversion of edf to bids compliant files.""" -import json -import os -import subprocess - -import numpy as np -import pandas as pd -import yaml -from yaml.loader import SafeLoader - -# CONVERSION events - -edf_filepath = input("Enter the edf file path: ") -if os.path.exists(edf_filepath): - print("The file exists") -else: - raise FileNotFoundError("No such file or directory") - - -subprocess.run(["edf2asc", "-y", "-e", edf_filepath, edf_filepath + "_events"]) -asc_file = edf_filepath + "_events.asc" - -with open(asc_file) as f: - events = f.readlines() - -# Read variables from the additional metadata txt file -print( - "Next: Load the manual_metadata.yml file with the additional metadata. \ - This file must contain at least the additional REQUIRED metadata \ - in the format specified in the BIDS specification. \ - Please enter the required metadata manually before loading the file in a next step." -) - -manual_metadata = input("Enter the file path to the manual_metadata.yml file: ") - -if os.path.exists(manual_metadata): - print("The file exists") -else: - raise FileNotFoundError("No such file or directory") - - -with open(manual_metadata) as f: - data = yaml.load(f, Loader=SafeLoader) - -SampleCoordinateUnits = data["SampleCoordinateUnits"] -SampleCoordinateSystem = data["SampleCoordinateSystem"] -EnvironmentCoordinates = data["EnvironmentCoordinates"] -ScreenDistance = data["ScreenDistance"] -ScreenRefreshRate = data["ScreenRefreshRate"] -ScreenSize = data["ScreenSize"] -InstitutionName = data["InstitutionName"] -InstitutionAddress = data["InstitutionAddress"] -SoftwareVersion = data["SoftwareVersion"] -ScreenAOIDefinition = data["ScreenAOIDefinition"] -EyeCameraSettings = data["EyeCameraSettings"] -EyeTrackerDistance = data["EyeTrackerDistance"] -FeatureDetectionSettings = data["FeatureDetectionSettings"] -GazeMappingSettings = data["GazeMappingSettings"] -RawDataFilters = data["RawDataFilters"] - -# Prepare asc file -# dataframe for events, all -df_ms = pd.DataFrame([ms.split() for ms in events if ms.startswith("MSG")]) - -# reduced dataframe without MSG and sample columns -df_ms_reduced = pd.DataFrame(df_ms.iloc[0:, 2:]) - -# Events.json Metadata - -ScreenResolution = list( - map( - int, - ( - df_ms_reduced[df_ms_reduced[2] == "DISPLAY_COORDS"] - .iloc[0:1, 3:5] - .to_string(header=False, index=False) - ).split(" "), - ) -) - -TaskName = ( - " ".join([ts for ts in events if ts.startswith("** RECORDED BY")]) - .replace("** RECORDED BY ", "") - .replace("\n", "") -) - -# Eyetrack.json Metadata - -ManufacturersModelName = ( - " ".join([ml for ml in events if ml.startswith("** EYELINK")]) - .replace("** ", "") - .replace("\n", "") -) - -DeviceSerialNumber = ( - " ".join([sl for sl in events if sl.startswith("** SERIAL NUMBER:")]) - .replace("** SERIAL NUMBER: ", "") - .replace("\n", "") -) - -SamplingFrequency = int( - df_ms_reduced[df_ms_reduced[2] == "RECCFG"] - .iloc[0:1, 2:3] - .to_string(header=False, index=False) -) - -eye = ( - df_ms_reduced[df_ms_reduced[2] == "RECCFG"] - .iloc[0:1, 5:6] - .to_string(header=False, index=False) -) -if eye == "L": - RecordedEye = eye.replace("L", "Left") -elif eye == "R": - RecordedEye = eye.replace("R", "Right") -elif eye == "LR": - RecordedEye = eye.replace("LR", "Both") - -if df_ms[df_ms[3] == "VALIDATION"].empty is False: - AverageCalibrationError = ( - np.array(df_ms[df_ms[3] == "VALIDATION"][[9]]).astype(float).tolist() - ) -else: - AverageCalibrationError = [] - -CalibrationCount = len(df_ms_reduced[df_ms_reduced[3] == "CALIBRATION"]) - -cal_pos = ( - np.array(df_ms_reduced[df_ms_reduced[2] == "VALIDATE"][8].str.split(",", expand=True)) - .astype(int) - .tolist() -) -cal_num = int(len(cal_pos) / CalibrationCount) -CalibrationPosition = list() -if len(cal_pos) != 0: - for i in range(0, len(cal_pos), cal_num): - CalibrationPosition.append(cal_pos[i : i + cal_num]) - -CalibrationType = ( - df_ms_reduced[df_ms_reduced[3] == "CALIBRATION"] - .iloc[0:1, 2:3] - .to_string(header=False, index=False) -) - -if len(cal_pos) != 0: - cal_unit = ( - (df_ms_reduced[df_ms_reduced[2] == "VALIDATE"][[13]]) - .iloc[0:1, 0:1] - .to_string(header=False, index=False) - ) - if cal_unit == "pix.": - CalibrationUnit = "pixel" - elif cal_unit == "mm": - CalibrationUnit = "mm" - elif cal_unit == "cm": - CalibrationUnit = "cm" -else: - CalibrationUnit = "" - -EyeTrackingMethod = ( - pd.DataFrame( - " ".join([tm for tm in events if tm.startswith(">>>>>>>")]) - .replace(")", ",") - .split(",") - ) - .iloc[1:2] - .to_string(header=False, index=False) -) - -if df_ms[df_ms[3] == "VALIDATION"].empty is False: - MaximalCalibrationError = ( - np.array(df_ms[df_ms[3] == "VALIDATION"][[11]]).astype(float).tolist() - ) -else: - MaximalCalibrationError = [] - - -PupilFitMethod = ( - (df_ms_reduced[df_ms_reduced[2] == "ELCL_PROC"]) - .iloc[0:1, 1:2] - .to_string(header=False, index=False) -) - -StartTime = ( - np.array(pd.DataFrame([st.split() for st in events if st.startswith("START")])[1]) - .astype(int) - .tolist() -) # TODO:figure out if this is actually the StartTime meant by the specification - -StopTime = ( - np.array(pd.DataFrame([so.split() for so in events if so.startswith("END")])[1]) - .astype(int) - .tolist() -) # TODO:figure out if this is actually the StopTime meant by the specification - -# to json - -out_filepath = input("Enter the subject file path: ") - -eyetrack_json = { - "Manufacturer": "SR-Research", - "ManufacturersModelName": ManufacturersModelName, - "DeviceSerialNumber": DeviceSerialNumber, - "SoftwareVersion": SoftwareVersion, - "SamplingFrequency": SamplingFrequency, - "SampleCoordinateUnits": SampleCoordinateUnits, - "SampleCoordinateSystem": SampleCoordinateSystem, - "EnvironmentCoordinates": EnvironmentCoordinates, - "RecordedEye": RecordedEye, - "ScreenAOIDefinition": ScreenAOIDefinition, - "AverageCalibrationError": AverageCalibrationError, - "CalibrationCount": CalibrationCount, - "CalibrationType": CalibrationType, - "CalibrationUnit": CalibrationUnit, - "EyeCameraSettings": EyeCameraSettings, - "EyeTrackerDistance": EyeTrackerDistance, - "CalibrationPosition": CalibrationPosition, - "EyeTrackingMethod": EyeTrackingMethod, - "FeatureDetectionSettings": FeatureDetectionSettings, - "GazeMappingSettings": GazeMappingSettings, - "MaximalCalibrationError": MaximalCalibrationError, - "PupilFitMethod": PupilFitMethod, - "RawDataFilters": RawDataFilters, - "StartTime": StartTime, - "StopTime": StopTime, -} - -with open(out_filepath + "eyetrack.json", "w") as outfile: - json.dump(eyetrack_json, outfile, indent=15) - -events_json = { - "TaskName": TaskName, - "InstitutionName": InstitutionName, - "InstitutionAddress": InstitutionAddress, - "StimulusPresentation": { - "ScreenDistance": ScreenDistance, - "ScreenRefreshRate": ScreenRefreshRate, - "ScreenResolution": ScreenResolution, - "ScreenSize": ScreenSize, - }, -} - -with open(out_filepath + "events.json", "w") as outfile: - json.dump(events_json, outfile, indent=9) diff --git a/eye2bids/__init__.py b/eye2bids/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/conversion_json/manual_metadata.yml b/eye2bids/config/metadata.yml similarity index 73% rename from conversion_json/manual_metadata.yml rename to eye2bids/config/metadata.yml index 1760fc5..fcd936c 100644 --- a/conversion_json/manual_metadata.yml +++ b/eye2bids/config/metadata.yml @@ -1,7 +1,7 @@ --- -#manual_metadata +# manual_metadata -#Required, fill according to BIDS specification: +# Required, fill according to BIDS specification: SampleCoordinateUnits: '' SampleCoordinateSystem: '' EnvironmentCoordinates: '' @@ -11,7 +11,7 @@ ScreenSize: InstitutionName: '' InstitutionAddress: '' -#Recommended, leave empty if not available: +# Recommended, leave empty if not available: SoftwareVersion: '' ScreenAOIDefinition: EyeCameraSettings: diff --git a/eye2bids/edf2bids.py b/eye2bids/edf2bids.py new file mode 100644 index 0000000..309b6fb --- /dev/null +++ b/eye2bids/edf2bids.py @@ -0,0 +1,251 @@ +"""Main module for conversion of edf to bids compliant files.""" +import json +import os +import subprocess + +import numpy as np +import pandas as pd +import yaml +from yaml.loader import SafeLoader + + +def main(): + """Convert edf to tsv + json.""" + # CONVERSION events + + edf_filepath = input("Enter the edf file path: ") + if os.path.exists(edf_filepath): + print("The file exists") + else: + raise FileNotFoundError("No such file or directory") + + subprocess.run(["edf2asc", "-y", "-e", edf_filepath, edf_filepath + "_events"]) + asc_file = edf_filepath + "_events.asc" + + with open(asc_file) as f: + events = f.readlines() + + # Read variables from the additional metadata txt file + print( + """Load the manual_metadata.yml file with the additional metadata.\n +This file must contain at least the additional REQUIRED metadata +in the format specified in the BIDS specification.\n +Please enter the required metadata manually +before loading the file in a next step.""" + ) + + manual_metadata = input("Enter the file path to the manual_metadata.yml file: ") + + if os.path.exists(manual_metadata): + print("The file exists") + else: + raise FileNotFoundError("No such file or directory") + + with open(manual_metadata) as f: + data = yaml.load(f, Loader=SafeLoader) + + SampleCoordinateUnits = data["SampleCoordinateUnits"] + SampleCoordinateSystem = data["SampleCoordinateSystem"] + EnvironmentCoordinates = data["EnvironmentCoordinates"] + ScreenDistance = data["ScreenDistance"] + ScreenRefreshRate = data["ScreenRefreshRate"] + ScreenSize = data["ScreenSize"] + InstitutionName = data["InstitutionName"] + InstitutionAddress = data["InstitutionAddress"] + SoftwareVersion = data["SoftwareVersion"] + ScreenAOIDefinition = data["ScreenAOIDefinition"] + EyeCameraSettings = data["EyeCameraSettings"] + EyeTrackerDistance = data["EyeTrackerDistance"] + FeatureDetectionSettings = data["FeatureDetectionSettings"] + GazeMappingSettings = data["GazeMappingSettings"] + RawDataFilters = data["RawDataFilters"] + + # Prepare asc file + # dataframe for events, all + df_ms = pd.DataFrame([ms.split() for ms in events if ms.startswith("MSG")]) + + # reduced dataframe without MSG and sample columns + df_ms_reduced = pd.DataFrame(df_ms.iloc[0:, 2:]) + + # Events.json Metadata + + ScreenResolution = list( + map( + int, + ( + df_ms_reduced[df_ms_reduced[2] == "DISPLAY_COORDS"] + .iloc[0:1, 3:5] + .to_string(header=False, index=False) + ).split(" "), + ) + ) + + TaskName = ( + " ".join([ts for ts in events if ts.startswith("** RECORDED BY")]) + .replace("** RECORDED BY ", "") + .replace("\n", "") + ) + + # Eyetrack.json Metadata + + ManufacturersModelName = ( + " ".join([ml for ml in events if ml.startswith("** EYELINK")]) + .replace("** ", "") + .replace("\n", "") + ) + + DeviceSerialNumber = ( + " ".join([sl for sl in events if sl.startswith("** SERIAL NUMBER:")]) + .replace("** SERIAL NUMBER: ", "") + .replace("\n", "") + ) + + SamplingFrequency = int( + df_ms_reduced[df_ms_reduced[2] == "RECCFG"] + .iloc[0:1, 2:3] + .to_string(header=False, index=False) + ) + + eye = ( + df_ms_reduced[df_ms_reduced[2] == "RECCFG"] + .iloc[0:1, 5:6] + .to_string(header=False, index=False) + ) + if eye == "L": + RecordedEye = eye.replace("L", "Left") + elif eye == "R": + RecordedEye = eye.replace("R", "Right") + elif eye == "LR": + RecordedEye = eye.replace("LR", "Both") + + if df_ms[df_ms[3] == "VALIDATION"].empty is False: + AverageCalibrationError = ( + np.array(df_ms[df_ms[3] == "VALIDATION"][[9]]).astype(float).tolist() + ) + else: + AverageCalibrationError = [] + + CalibrationCount = len(df_ms_reduced[df_ms_reduced[3] == "CALIBRATION"]) + + cal_pos = ( + np.array( + df_ms_reduced[df_ms_reduced[2] == "VALIDATE"][8].str.split(",", expand=True) + ) + .astype(int) + .tolist() + ) + cal_num = int(len(cal_pos) / CalibrationCount) + CalibrationPosition = list() + if len(cal_pos) != 0: + for i in range(0, len(cal_pos), cal_num): + CalibrationPosition.append(cal_pos[i : i + cal_num]) + + CalibrationType = ( + df_ms_reduced[df_ms_reduced[3] == "CALIBRATION"] + .iloc[0:1, 2:3] + .to_string(header=False, index=False) + ) + + if len(cal_pos) != 0: + cal_unit = ( + (df_ms_reduced[df_ms_reduced[2] == "VALIDATE"][[13]]) + .iloc[0:1, 0:1] + .to_string(header=False, index=False) + ) + if cal_unit == "pix.": + CalibrationUnit = "pixel" + elif cal_unit == "mm": + CalibrationUnit = "mm" + elif cal_unit == "cm": + CalibrationUnit = "cm" + else: + CalibrationUnit = "" + + EyeTrackingMethod = ( + pd.DataFrame( + " ".join([tm for tm in events if tm.startswith(">>>>>>>")]) + .replace(")", ",") + .split(",") + ) + .iloc[1:2] + .to_string(header=False, index=False) + ) + + if df_ms[df_ms[3] == "VALIDATION"].empty is False: + MaximalCalibrationError = ( + np.array(df_ms[df_ms[3] == "VALIDATION"][[11]]).astype(float).tolist() + ) + else: + MaximalCalibrationError = [] + + PupilFitMethod = ( + (df_ms_reduced[df_ms_reduced[2] == "ELCL_PROC"]) + .iloc[0:1, 1:2] + .to_string(header=False, index=False) + ) + + StartTime = ( + np.array(pd.DataFrame([st.split() for st in events if st.startswith("START")])[1]) + .astype(int) + .tolist() + ) # TODO:figure out if this is actually the StartTime meant by the specification + + StopTime = ( + np.array(pd.DataFrame([so.split() for so in events if so.startswith("END")])[1]) + .astype(int) + .tolist() + ) # TODO:figure out if this is actually the StopTime meant by the specification + + # to json + + out_filepath = input("Enter the subject file path: ") + + eyetrack_json = { + "Manufacturer": "SR-Research", + "ManufacturersModelName": ManufacturersModelName, + "DeviceSerialNumber": DeviceSerialNumber, + "SoftwareVersion": SoftwareVersion, + "SamplingFrequency": SamplingFrequency, + "SampleCoordinateUnits": SampleCoordinateUnits, + "SampleCoordinateSystem": SampleCoordinateSystem, + "EnvironmentCoordinates": EnvironmentCoordinates, + "RecordedEye": RecordedEye, + "ScreenAOIDefinition": ScreenAOIDefinition, + "AverageCalibrationError": AverageCalibrationError, + "CalibrationCount": CalibrationCount, + "CalibrationType": CalibrationType, + "CalibrationUnit": CalibrationUnit, + "EyeCameraSettings": EyeCameraSettings, + "EyeTrackerDistance": EyeTrackerDistance, + "CalibrationPosition": CalibrationPosition, + "EyeTrackingMethod": EyeTrackingMethod, + "FeatureDetectionSettings": FeatureDetectionSettings, + "GazeMappingSettings": GazeMappingSettings, + "MaximalCalibrationError": MaximalCalibrationError, + "PupilFitMethod": PupilFitMethod, + "RawDataFilters": RawDataFilters, + "StartTime": StartTime, + "StopTime": StopTime, + } + + with open(out_filepath + "eyetrack.json", "w") as outfile: + json.dump(eyetrack_json, outfile, indent=15) + + events_json = { + "TaskName": TaskName, + "InstitutionName": InstitutionName, + "InstitutionAddress": InstitutionAddress, + "StimulusPresentation": { + "ScreenDistance": ScreenDistance, + "ScreenRefreshRate": ScreenRefreshRate, + "ScreenResolution": ScreenResolution, + "ScreenSize": ScreenSize, + }, + } + + with open(out_filepath + "events.json", "w") as outfile: + json.dump(events_json, outfile, indent=9) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index a6a0c83..4396c5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,64 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling", "hatch-vcs"] + +[project] +authors = [{name = "eye2bids developers"}] +classifiers = [ + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "License :: OSI Approved", + "Programming Language :: Python", + "Topic :: Software Development", + "Topic :: Scientific/Engineering", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11" +] +dependencies = [ + "numpy", + "pandas" +] +description = "eyetracking data bids converter in Python" +dynamic = ["version"] +license = {text = "MIT"} +name = "eye2bids" +readme = "README.md" +requires-python = ">=3.8" + +[project.optional-dependencies] +# A combination of dependencies useful for developers +dev = [ + "eye2bids[test,doc]", + "isort", + "black", + "flake8", + "flake8-use-fstring", + "flake8-functions", + "flake8-docstrings", + 'codespell', + 'tomli', + 'pre-commit' +] +# Requirements necessary for building the documentation +doc = [ + "myst-parser", + "numpydoc", + "sphinx", + "sphinx-copybutton" +] +# For running unit and docstring tests +test = [ + "coverage", + "pytest", + "pytest-cov" +] + [tool.black] line-length = 90 @@ -5,8 +66,22 @@ line-length = 90 ignore-words = ".github/codespell_ignore_words.txt" skip = "./.git,.mypy_cache,env,venv,*/tmp,./doc/_build" +[tool.hatch.build.hooks.vcs] +version-file = "eye2bids/_version.py" + +[tool.hatch.build.targets.wheel] +packages = ["eye2bids"] + +[tool.hatch.version] +source = "vcs" + [tool.isort] combine_as_imports = true line_length = 90 profile = "black" skip_gitignore = true + +[tool.pytest.ini_options] +addopts = "-ra -q -vv --cov eye2bids" +norecursedirs = "data" +testpaths = ["tests/"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/conversion_json/manual_metadata_example.yml b/tests/data/manual_metadata_example.yml similarity index 80% rename from conversion_json/manual_metadata_example.yml rename to tests/data/manual_metadata_example.yml index c70c94c..79ead08 100644 --- a/conversion_json/manual_metadata_example.yml +++ b/tests/data/manual_metadata_example.yml @@ -1,7 +1,7 @@ --- -#manual_metadata +# manual_metadata -#Required, fill according to BIDS specification: +# Required, fill according to BIDS specification: SampleCoordinateUnits: pixel SampleCoordinateSystem: gaze-on-screen EnvironmentCoordinates: top-left @@ -11,7 +11,7 @@ ScreenSize: [38.6, 29] InstitutionName: Philipps-University Marburg InstitutionAddress: Gutenbergstr. 18 -#Recommended, leave empty if not available: +# Recommended, leave empty if not available: SoftwareVersion: SREB1.10.1630 WIN32 LID:F2AE011 Mod:2017.04.21 15:19 CEST ScreenAOIDefinition: [square, [00, 150, 300, 350]] EyeCameraSettings: