Source code for roiextractors.extractors.miniscopeimagingextractor.miniscopeimagingextractor
"""MiniscopeImagingExtractor class.
Classes
-------
MiniscopeImagingExtractor
An ImagingExtractor for the Miniscope video (.avi) format.
"""
import json
import warnings
from datetime import datetime
from pathlib import Path
import numpy as np
from ...extraction_tools import PathType, get_package
from ...imagingextractor import ImagingExtractor
from ...multiimagingextractor import MultiImagingExtractor
def read_timestamps_from_csv_file(file_path: PathType) -> np.ndarray:
"""
Retrieve the timestamps from a CSV file.
Parameters
----------
file_path : PathType
Path to the CSV file containing the timestamps relative to the recording start.
Returns
-------
timestamps
The timestamps extracted from the CSV file.
"""
import pandas as pd
file_path = str(file_path)
timestamps = pd.read_csv(file_path)["Time Stamp (ms)"].values.astype(float)
timestamps /= 1000
return timestamps
[docs]
class MiniscopeImagingExtractor(MultiImagingExtractor):
"""
Extractor for Miniscope calcium imaging data recorded with Miniscope-DAQ-QT-Software.
This extractor consolidates multiple .avi video files from a Miniscope recording session
into a single continuous dataset. It uses hardware-generated timestamps from timeStamps.csv
for accurate timing information.
The extractor works at the device folder level, where a typical Miniscope recording has
the following structure:
device_folder/ (e.g., "Miniscope", "HPC_miniscope1", "ACC_miniscope2")
├── 0.avi, 1.avi, 2.avi, ... # Video files (FFV1 lossless codec)
├── timeStamps.csv # Hardware-generated timestamps (required)
├── metaData.json # Device configuration (optional, for reference)
└── headOrientation.csv # IMU data (optional)
Key Features
------------
- Automatically discovers and concatenates multiple .avi files from a recording
- Calculates sampling frequency from hardware timestamps (timeStamps.csv)
- Supports both folder-based and file-list initialization
- Provides access to device and session metadata via static methods
See Also
--------
MiniscopeMultiRecordingImagingExtractor : For multi-session recordings (Tye Lab format)
"""
def __init__(
self,
folder_path: PathType | None = None,
file_paths: list[PathType] | None = None,
configuration_file_path: PathType | None = None, # Ignored, pending removal in neuroconv (May 2026)
timestamps_path: PathType | None = None,
*,
sampling_frequency: float | None = None,
):
"""
Initialize MiniscopeImagingExtractor.
Parameters
----------
folder_path : PathType | None, optional
Path to the device folder containing the Miniscope recording files (.avi videos,
metaData.json, and timeStamps.csv). This is the recommended way to initialize the
extractor as it automatically discovers all necessary files.
Note: This is the device-level folder (e.g., "HPC_miniscope1"), not the session
folder (which may contain multiple device folders).
file_paths : list[PathType] | None, optional
List of .avi file paths to be processed. These files should be from the same
recording session and will be concatenated in the order provided.
configuration_file_path : PathType | None, optional
Deprecated. This parameter is ignored. Kept temporarily for backwards compatibility
with neuroconv, which still forwards it. Will be removed on or after May 2026.
timestamps_path : PathType | None, optional
Path to the timeStamps.csv file containing timestamps relative to the recording start.
If not provided, the extractor will look for a timeStamps.csv file in the same directory
as the video files as a fallback. This file is required for accurate timing.
Default is None.
sampling_frequency : float | None, optional
Explicit sampling frequency in Hz. If provided, this overrides the calculated value
from timeStamps.csv. Only use this if you have a specific reason to override the
measured sampling frequency (e.g., working with incomplete data).
Default is None (calculate from timeStamps.csv).
Examples
--------
>>> # Recommended: Folder-based initialization (auto-detects .avi files and timeStamps.csv)
>>> folder_path = "/path/to/miniscope_folder"
>>> extractor = MiniscopeImagingExtractor(folder_path=folder_path)
>>> # Direct file specification (device folder inferred from file_paths)
>>> file_paths = ["/path/to/device_folder/0.avi", "/path/to/device_folder/1.avi"]
>>> extractor = MiniscopeImagingExtractor(file_paths=file_paths)
>>> # With explicit timestamps path
>>> timestamps_path = "/path/to/device_folder/timeStamps.csv"
>>> extractor = MiniscopeImagingExtractor(file_paths=file_paths, timestamps_path=timestamps_path)
Notes
-----
For each video file, a _MiniscopeSingleVideoExtractor is created. These individual extractors
are then combined into the MiniscopeImagingExtractor to handle the session's recordings
as a unified, continuous dataset.
"""
# Determine file paths based on folder_path or provided arguments
if folder_path is not None:
if file_paths is not None:
raise ValueError(
"When folder_path is provided, file_paths cannot be specified. "
"Use either folder_path alone or provide file_paths."
)
file_paths, timestamps_path = self._get_miniscope_files_from_direct_folder(folder_path)
else:
if file_paths is None:
raise ValueError("When folder_path is not provided, file_paths must be specified.")
# Validate input files
self.validate_miniscope_files(file_paths, timestamps_path)
# Determine timestamps path
# If not provided, look for timeStamps.csv in the same folder as the video files
if timestamps_path is not None:
self._timestamps_path = Path(timestamps_path)
else:
# Infer device folder from file_paths (all .avi files are in the same folder)
device_folder = Path(file_paths[0]).parent
self._timestamps_path = device_folder / "timeStamps.csv"
# Determine sampling frequency with priority order:
# 1. Explicit sampling_frequency parameter (user override)
# 2. Calculated from timeStamps.csv (measured ground truth)
# 3. Error if timeStamps.csv is missing
if sampling_frequency is not None:
# User explicitly provided - use it
self._sampling_frequency = float(sampling_frequency)
# Still validate timestamps exist (for completeness checking)
if not self._timestamps_path.exists():
warnings.warn(
f"timeStamps.csv not found at {self._timestamps_path}. "
f"Using user-provided sampling_frequency={sampling_frequency} Hz. "
f"Note: Miniscope recordings should always include timeStamps.csv."
)
elif self._timestamps_path.exists():
# Calculate from timestamps (preferred path)
self._sampling_frequency = self._calculate_sampling_frequency_from_timestamps(
self._timestamps_path, num_samples=10_000
)
else:
# Missing timestamps - fail with helpful message
raise FileNotFoundError(
f"timeStamps.csv not found at {self._timestamps_path}. "
f"This file is required for accurate timing and should be automatically "
f"generated by Miniscope-DAQ-QT-Software. Possible causes:\n"
f" - Incomplete recording\n"
f" - Files copied without timeStamps.csv\n"
f" - Non-standard data acquisition setup\n\n"
f"If you have a specific reason to proceed without timestamps, "
f"you can provide the sampling_frequency parameter explicitly:\n"
f" MiniscopeImagingExtractor(..., sampling_frequency=30.0)"
)
# Create individual extractors for each video file
imaging_extractors = []
for file_path in file_paths:
extractor = _MiniscopeSingleVideoExtractor(file_path=file_path)
extractor._sampling_frequency = self._sampling_frequency
imaging_extractors.append(extractor)
super().__init__(imaging_extractors=imaging_extractors)
def _calculate_sampling_frequency_from_timestamps(
self, timestamps_file_path: PathType, num_samples: int = 10_000
) -> float:
"""
Calculate sampling frequency from timeStamps.csv.
Parameters
----------
timestamps_file_path : PathType
Path to timeStamps.csv file
num_samples : int
Number of samples to read for calculation (default: 10_000).
Reading fewer samples makes this fast (~50ms) while still accurate.
Returns
-------
float
Measured sampling frequency in Hz
Notes
-----
- Reads only first `num_samples` rows for speed
- Removes outliers (>3 std dev) to handle dropped frames
- Expected precision: ±2-5% due to USB/OS jitter
"""
import pandas as pd
# Read only first N samples (fast)
df = pd.read_csv(timestamps_file_path, nrows=num_samples)
timestamps_ms = df["Time Stamp (ms)"].values
timestamps_s = timestamps_ms / 1000.0
if len(timestamps_s) < 2:
raise ValueError(
f"timeStamps.csv has insufficient data: only {len(timestamps_s)} frames. "
f"Need at least 2 frames to calculate sampling frequency."
)
# Calculate intervals
intervals = np.diff(timestamps_s)
# Remove outliers (e.g., dropped frames, buffer overruns)
mean_interval = np.mean(intervals)
std_interval = np.std(intervals)
valid_intervals = intervals[np.abs(intervals - mean_interval) < 3 * std_interval]
if len(valid_intervals) == 0:
raise ValueError(
f"Could not calculate sampling frequency from {timestamps_file_path}: "
f"all intervals appear to be outliers. The recording may be corrupted."
)
# Calculate sampling frequency
mean_interval_clean = np.mean(valid_intervals)
sampling_frequency = 1.0 / mean_interval_clean
return sampling_frequency
@staticmethod
def _get_miniscope_files_from_direct_folder(
folder_path: PathType,
) -> tuple[list[PathType], PathType, PathType | None]:
"""
Retrieve Miniscope files from a folder containing .avi files directly.
This function handles cases where .avi files and metaData.json are located
directly in the specified folder without subfolders.
Expected folder structure:
```
device_folder_path/
├── 0.avi
├── 1.avi
├── 2.avi
├── metaData.json
├── timeStamps.csv
└── headOrientation.csv
```
Parameters
----------
folder_path : PathType
Path to the folder containing .avi files and metaData.json directly.
Returns
-------
tuple[list[PathType], PathType | None]
- list[PathType]: .avi file paths sorted naturally
- PathType | None: path to the timestamps file (timeStamps.csv) if present, otherwise None
Raises
------
AssertionError
If no .avi files are found.
ValueError
If file names do not follow the expected sequential naming convention.
"""
natsort = get_package(package_name="natsort", installation_instructions="pip install natsort")
folder_path = Path(folder_path)
miniscope_avi_file_paths = natsort.natsorted(list(folder_path.glob("*.avi")))
miniscope_timestamps_files = natsort.natsorted(list(folder_path.glob("timeStamps.csv")))
assert miniscope_avi_file_paths, f"No .avi files found in direct folder structure at '{folder_path}'"
# check that the list of file paths follow the expected naming convention (0.avi, 1.avi, 2.avi, ...)
for i, file_path in enumerate(miniscope_avi_file_paths):
expected_file_name = f"{i}.avi"
if file_path.name != expected_file_name:
raise ValueError(
f"Unexpected file name '{file_path.name}'. Expected '{expected_file_name}'. "
"Ensure .avi files are named sequentially starting from 0 (e.g., 0.avi, 1.avi, 2.avi, ...)."
)
# timestamps file is optional
if miniscope_timestamps_files:
assert (
len(miniscope_timestamps_files) == 1
), f"Multiple timestamps files found at '{folder_path}', expected only one 'timeStamps.csv'"
timestamps_path = miniscope_timestamps_files[0]
else:
timestamps_path = None
return miniscope_avi_file_paths, timestamps_path
[docs]
@staticmethod
def validate_miniscope_files(file_paths: list[PathType], timestamps_path: PathType | None = None) -> None:
"""
Validate that the provided Miniscope files exist and are accessible.
Parameters
----------
file_paths : List[PathType]
List of .avi file paths to validate.
timestamps_path : PathType | None
Path to the timestamps file to validate, by default None.
Raises
------
FileNotFoundError
If any of the specified files do not exist.
ValueError
If the file lists are empty or contain invalid file types.
Notes
-----
Only the .avi files are strictly required. The timestamps file is optional.
"""
if not file_paths:
raise ValueError("file_paths cannot be empty.")
for file_path in file_paths:
file_path = Path(file_path)
if not file_path.exists():
raise FileNotFoundError(f"Video file not found: {file_path}")
if not file_path.suffix == ".avi":
raise ValueError(f"Video files must be .avi files, got: {file_path}")
if timestamps_path is not None:
timestamps_path = Path(timestamps_path)
if not timestamps_path.exists():
raise FileNotFoundError(f"Timestamps file not found: {timestamps_path}")
if not timestamps_path.suffix == ".csv":
raise ValueError(f"Timestamps file must be a .csv file, got: {timestamps_path}")
[docs]
@staticmethod
def load_miniscope_config(configuration_file_path: PathType) -> dict:
"""
Load and parse the Miniscope configuration file.
This is a generic method that can read any metaData.json file (session or device level).
For more explicit metadata reading with specialized documentation, consider using:
- _read_session_folder_metadata() for session-level metadata
- _read_device_folder_metadata() for device-level metadata
Parameters
----------
configuration_file_path : PathType
Path to the metaData.json configuration file.
Returns
-------
dict
Parsed configuration data from the JSON file.
Raises
------
FileNotFoundError
If the configuration file does not exist.
json.JSONDecodeError
If the configuration file is not valid JSON.
"""
configuration_file_path = Path(configuration_file_path)
if not configuration_file_path.exists():
raise FileNotFoundError(f"Configuration file not found: {configuration_file_path}")
try:
with open(configuration_file_path, "r") as f:
return json.load(f)
except json.JSONDecodeError as e:
raise json.JSONDecodeError(
f"Invalid JSON in configuration file {configuration_file_path}: {e}", e.doc, e.pos
)
@staticmethod
def _read_device_folder_metadata(metadata_file_path: PathType) -> dict:
"""
Read device-level metaData.json containing Miniscope hardware configuration.
The Miniscope-DAQ-QT-Software creates two levels of metaData.json files:
1. Session-level: Contains experiment/animal info and recording start time
2. Device-level: Contains hardware settings for each Miniscope/camera
This method reads the DEVICE-level metadata.
File Location
-------------
Device metadata is located in each device's subfolder:
```
session_folder/
├── metaData.json # Session-level (use read_session_folder_metadata)
├── HPC_miniscope1/ # Device folder
│ ├── metaData.json # Device-level (this method)
│ ├── timeStamps.csv
│ └── 0.avi, 1.avi, ...
└── ACC_miniscope2/ # Another device
├── metaData.json # Device-level (this method)
└── ...
```
Device Metadata Contents
------------------------
{
"deviceType": "Miniscope_V4_BNO", # Hardware version
"deviceName": "HPC_miniscope1", # User-assigned name
"deviceID": 1, # Numeric ID
"frameRate": "30FPS", # Configured frame rate
"compression": "FFV1", # Video codec
"framesPerFile": 1000, # Frames per AVI file
"gain": "Medium", # Sensor gain setting
"ewl": 70, # Excitation wavelength
"led0": 24, # LED power (0-100)
"ROI": { # Region of interest (optional)
"height": 608,
"width": 608,
"leftEdge": 0,
"topEdge": 0
}
}
Parameters
----------
metadata_file_path : PathType
Path to the device metaData.json file
(e.g., "session_folder/HPC_miniscope1/metaData.json")
Returns
-------
dict
Device metadata containing hardware configuration and acquisition parameters.
Fields include:
- deviceType: Hardware version (e.g., "Miniscope_V4_BNO", "Miniscope_V3")
- deviceName: User-assigned device name
- deviceID: Numeric device identifier
- frameRate: Configured frame rate (NOTE: may not match actual rate)
- compression: Video compression codec
- framesPerFile: Number of frames per video file
- gain: Sensor gain setting
- ewl: Excitation wavelength (electrowetting lens position)
- led0: LED power setting
- ROI: Region of interest settings (if used)
Raises
------
FileNotFoundError
If metaData.json file is not found
Examples
--------
>>> metadata = MiniscopeImagingExtractor.read_device_folder_metadata(
... "path/to/session/HPC_miniscope1/metaData.json"
... )
>>> print(f"Device: {metadata['deviceType']}")
Device: Miniscope_V4_BNO
>>> print(f"Gain: {metadata['gain']}, LED: {metadata['led0']}")
Gain: Medium, LED: 24
Notes
-----
- The frameRate field is user-configured and may not reflect the actual
acquisition rate. Use timeStamps.csv for ground truth timing.
- Acquisition parameters (gain, ewl, led0) are captured at recording start
and may have been adjusted during the session.
See Also
--------
read_session_folder_metadata : Read session-level metadata
"""
metadata_file_path = Path(metadata_file_path)
if not metadata_file_path.exists():
raise FileNotFoundError(f"Device metadata file not found: {metadata_file_path}")
with open(metadata_file_path, "r") as f:
return json.load(f)
@staticmethod
def _read_session_folder_metadata(metadata_file_path: PathType) -> dict:
"""
Read session-level metaData.json containing experiment and recording information.
The Miniscope-DAQ-QT-Software creates two levels of metaData.json files:
1. Session-level: Contains experiment/animal info and recording start time (this method)
2. Device-level: Contains hardware settings for each Miniscope/camera
This method reads the SESSION-level metadata.
File Location
-------------
Session metadata is located in the recording session's root folder:
```
session_folder/ # Recording session
├── metaData.json # Session-level (this method)
├── notes.csv # User notes with timestamps
├── HPC_miniscope1/ # Device folder
│ ├── metaData.json # Device-level (use read_device_folder_metadata)
│ └── ...
└── ACC_miniscope2/ # Another device folder
└── ...
```
Session Metadata Contents
-------------------------
{
"researcherName": "researcher_name", # Researcher identifier
"animalName": "animal_name", # Subject identifier
"experimentName": "experiment_name", # Experiment identifier
"baseDirectory": "D:/path/to/session", # Full path to session
"recordingStartTime": { # Timestamp when recording started
"year": 2025,
"month": 6,
"day": 12,
"hour": 15,
"minute": 26,
"second": 31,
"msec": 176,
"msecSinceEpoch": 1749756391176 # Unix timestamp in milliseconds
},
"miniscopes": [ # List of Miniscope device names
"HPC_miniscope1",
"ACC_miniscope2"
],
"cameras": [ # List of behavior camera names
"BehavCam_1"
],
"framesPerFile": 1000 # Default frames per video file
}
Parameters
----------
metadata_file_path : PathType
Path to the session metaData.json file
(e.g., "path/to/2025_06_12/15_26_31/metaData.json")
Returns
-------
dict
Session metadata containing experiment information and recording details.
Fields include:
- researcherName: Researcher identifier
- animalName: Subject/animal identifier
- experimentName: Experiment identifier
- baseDirectory: Original recording path
- recordingStartTime: Recording start timestamp (dict with year, month, day, etc.)
- miniscopes: List of Miniscope device names in this session
- cameras: List of behavior camera names in this session
- framesPerFile: Default frames per video file
Raises
------
FileNotFoundError
If metaData.json file is not found
Examples
--------
>>> metadata = MiniscopeImagingExtractor.read_session_folder_metadata(
... "path/to/2025_06_12/15_26_31/metaData.json"
... )
>>> print(f"Experiment: {metadata['experimentName']}")
Experiment: experiment_name
>>> print(f"Devices: {', '.join(metadata['miniscopes'])}")
Devices: HPC_miniscope1, ACC_miniscope2
>>> start_time = metadata['recordingStartTime']
>>> print(f"Started: {start_time['year']}-{start_time['month']}-{start_time['day']}")
Started: 2025-6-12
Notes
-----
- The recordingStartTime is when the DAQ software started recording,
not when individual frames were captured (use timeStamps.csv for that)
- Device lists (miniscopes, cameras) reflect what was configured,
not necessarily what has complete data
See Also
--------
read_device_folder_metadata : Read device-level metadata
"""
metadata_file_path = Path(metadata_file_path)
if not metadata_file_path.exists():
raise FileNotFoundError(f"Session metadata file not found: {metadata_file_path}")
with open(metadata_file_path, "r") as f:
return json.load(f)
[docs]
def get_native_timestamps(
self, start_sample: int | None = None, end_sample: int | None = None
) -> np.ndarray | None:
if self._timestamps_path is None:
warnings.warn("Timestamps file not provided or not found. Returning None for timestamps.")
return None
# Set defaults
if start_sample is None:
start_sample = 0
if end_sample is None:
end_sample = self.get_num_samples()
# Read timestamps from CSV file
native_timestamps = read_timestamps_from_csv_file(self._timestamps_path)
return native_timestamps[start_sample:end_sample]
[docs]
def has_time_vector(self) -> bool:
"""Detect if the ImagingExtractor has a time vector set or not.
Notes
-----
Miniscope recordings should always have native timestamps from timeStamps.csv.
This method overrides the parent implementation to ensure timestamps are properly
loaded and returned, as Miniscope data is fundamentally time-based with
hardware-generated timestamps that provide ground truth timing.
Returns
-------
has_times: bool
True if the ImagingExtractor has a time vector set, otherwise False.
"""
if self._times is None:
self._times = self.get_native_timestamps()
return self._times is not None
@staticmethod
def _get_session_start_time(miniscope_folder_path) -> datetime | None:
from .miniscope_utils import get_recording_start_time
try:
session_start_time = get_recording_start_time(file_path=Path(miniscope_folder_path) / "metaData.json")
return session_start_time
except (FileNotFoundError, KeyError, json.JSONDecodeError) as e:
warnings.warn(f"Could not retrieve session start time for folder {miniscope_folder_path}: \n {e}")
return None
# Temporary renaming to keep backwards compatibility
class MiniscopeMultiRecordingImagingExtractor(MiniscopeImagingExtractor):
"""
MiniscopeMultiRecordingImagingExtractor processes multiple separate Miniscope recordings within the same session.
This extractor consolidates the recordings as a single continuous dataset.
Parameters
----------
folder_path : PathType
The folder path containing the Miniscope video (.avi) files and the metaData.json configuration file.
miniscope_device_name : str, optional
The name of the Miniscope device subfolder. Default is "Miniscope".
Notes
-----
This extractor is designed to handle the Tye Lab format, where multiple recordings
are organized in timestamp subfolders, each containing a Miniscope subfolder.
The expected folder structure is as follows:
```
parent_folder/
├── 15_03_28/ (timestamp folder)
│ ├── Miniscope/ (miniscope_device_name folder)
│ │ ├── 0.avi
│ │ ├── 1.avi
│ │ └── metaData.json
│ ├── BehavCam_2/
│ └── metaData.json
├── 15_06_28/ (timestamp folder)
│ ├── Miniscope/ (miniscope_device_name folder)
│ │ ├── 0.avi
│ │ └── metaData.json
│ └── BehavCam_2/
└── 15_12_28/ (timestamp folder)
└── Miniscope/ (miniscope_device_name folder)
├── 0.avi
└── metaData.json
```
This extractor will automatically find all the .avi files and the metaData.json configuration file
within the specified folder and its subfolders, and create a _MiniscopeSingleVideoExtractor for each .avi file.
The individual extractors are then combined into the MiniscopeMultiRecordingImagingExtractor to handle
the session's recordings as a unified, continuous dataset.
"""
extractor_name = "MiniscopeMultiRecordingImagingExtractor"
def __init__(self, folder_path: PathType, miniscope_device_name: str = "Miniscope"):
"""Create a MiniscopeMultiRecordingImagingExtractor instance from folder_path."""
warnings.warn(
"MiniscopeMultiRecordingImagingExtractor is deprecated and will be removed in or after December 2026. "
"Use MiniscopeImagingExtractor with explicit file_paths instead.",
FutureWarning,
stacklevel=2,
)
self.miniscope_device_name = miniscope_device_name
self.folder_path = Path(folder_path)
file_paths = self._get_miniscope_files_from_multi_recordings_subfolders(folder_path, miniscope_device_name)
super().__init__(file_paths=file_paths)
@staticmethod
def _get_miniscope_files_from_multi_recordings_subfolders(
folder_path: PathType, miniscope_device_name: str = "Miniscope"
) -> tuple[list[PathType], PathType]:
"""
Retrieve Miniscope files from a multi-session folder structure.
This function handles the Tye Lab format where multiple recordings
are organized in timestamp subfolders, each containing a Miniscope subfolder.
Expected folder structure:
```
parent_folder/
├── 15_03_28/ (timestamp folder)
│ ├── Miniscope/
│ │ ├── 0.avi
│ │ ├── 1.avi
│ │ └── metaData.json
│ ├── BehavCam_2/
│ └── metaData.json
├── 15_06_28/ (timestamp folder)
│ ├── Miniscope/
│ │ ├── 0.avi
│ │ └── metaData.json
│ └── BehavCam_2/
└── 15_12_28/ (timestamp folder)
└── Miniscope/
├── 0.avi
└── metaData.json
```
Parameters
----------
folder_path : PathType
Path to the parent folder containing timestamp subfolders.
miniscopeDeviceName : str, optional
Name of the Miniscope device subfolder. Defaults to "Miniscope".
Returns
-------
list[PathType]
List of .avi file paths sorted naturally.
Raises
------
AssertionError
If no .avi files are found.
"""
from pathlib import Path
from ...extraction_tools import get_package
natsort = get_package(package_name="natsort", installation_instructions="pip install natsort")
folder_path = Path(folder_path)
miniscope_avi_file_paths = natsort.natsorted(list(folder_path.glob(f"*/{miniscope_device_name}/*.avi")))
assert miniscope_avi_file_paths, f"No Miniscope .avi files found at '{folder_path}'"
return miniscope_avi_file_paths
def get_native_timestamps(
self, start_sample: int | None = None, end_sample: int | None = None
) -> np.ndarray | None:
"""
Retrieve timestamps for multiple recordings in a multi-recordings folder structure.
Returns
-------
np.ndarray | None
An array of floats representing the timestamps for the recordings.
Raises
------
AssertionError
If no time files are found.
"""
from .miniscope_utils import get_recording_start_times_for_multi_recordings
natsort = get_package(package_name="natsort", installation_instructions="pip install natsort")
time_file_name = "timeStamps.csv"
timestamps_file_paths = natsort.natsorted(
list(self.folder_path.glob(f"*/{self.miniscope_device_name}/{time_file_name}"))
)
assert timestamps_file_paths, f"No time files found at '{self.folder_path}'"
recording_start_times = get_recording_start_times_for_multi_recordings(folder_path=self.folder_path)
timestamps = []
for file_ind, file_path in enumerate(timestamps_file_paths):
timestamps_per_file = read_timestamps_from_csv_file(file_path=file_path)
if recording_start_times:
offset = (recording_start_times[file_ind] - recording_start_times[0]).total_seconds()
timestamps_per_file += offset
timestamps.extend(timestamps_per_file)
# Set defaults
if start_sample is None:
start_sample = 0
if end_sample is None:
end_sample = self.get_num_samples()
return np.array(timestamps)[start_sample:end_sample]
class _MiniscopeSingleVideoExtractor(ImagingExtractor):
"""An auxiliary extractor to get data from a single Miniscope video (.avi) file.
This format consists of a single video (.avi)
Multiple _MiniscopeSingleVideoExtractor are combined by downstream extractors to extract the data
"""
extractor_name = "_MiniscopeSingleVideo"
def __init__(self, file_path: PathType):
"""Create a _MiniscopeSingleVideoExtractor instance from a file path.
Parameters
----------
file_path: PathType
The file path to the Miniscope video (.avi) file.
"""
self._cv2 = get_package(package_name="cv2", installation_instructions="pip install opencv-python-headless")
self.file_path = file_path
super().__init__()
vc = self._cv2.VideoCapture(str(file_path))
self._num_samples = int(vc.get(self._cv2.CAP_PROP_FRAME_COUNT))
_, frame = vc.read()
self._image_size = frame.shape
self._dtype = frame.dtype
vc.release()
self._sampling_frequency = None
def get_num_samples(self) -> int:
return self._num_samples
def get_image_shape(self) -> tuple[int, int]:
"""Get the shape of the video frame (num_rows, num_columns).
Returns
-------
image_shape: tuple
Shape of the video frame (num_rows, num_columns).
"""
return self._image_size[:-1]
def get_sampling_frequency(self):
return self._sampling_frequency
def get_dtype(self) -> np.dtype:
return self._dtype
def get_series(self, start_sample: int | None = None, end_sample: int | None = None) -> np.ndarray:
end_sample = end_sample or self.get_num_samples()
start_sample = start_sample or 0
series = np.empty(shape=(end_sample - start_sample, *self.get_sample_shape()), dtype=self.get_dtype())
vc = self._cv2.VideoCapture(str(self.file_path))
vc.set(self._cv2.CAP_PROP_POS_FRAMES, start_sample)
for frame_number in range(end_sample - start_sample):
_, frame = vc.read()
series[frame_number] = self._cv2.cvtColor(frame, self._cv2.COLOR_BGR2GRAY)
vc.release()
return series
def get_native_timestamps(
self, start_sample: int | None = None, end_sample: int | None = None
) -> np.ndarray | None:
return None