Skip to content
2 changes: 1 addition & 1 deletion src/spyglass/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
PositionVideo,
TrackGraph,
)
from spyglass.common.common_region import BrainRegion
from spyglass.common.common_region import BrainRegion, CoordinateSystem
from spyglass.common.common_sensors import SensorData
from spyglass.common.common_session import Session
from spyglass.common.common_subject import Subject
Expand Down
71 changes: 67 additions & 4 deletions src/spyglass/common/common_region.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datajoint as dj

from spyglass.utils.dj_mixin import SpyglassMixin
from spyglass.utils.dj_mixin import SpyglassMixin, logger

schema = dj.schema("common_region")

Expand All @@ -10,9 +10,13 @@ class BrainRegion(SpyglassMixin, dj.Lookup):
definition = """
region_id: smallint auto_increment
---
region_name: varchar(200) # the name of the brain region
subregion_name=NULL: varchar(200) # subregion name
subsubregion_name=NULL: varchar(200) # subregion within subregion
region_name: varchar(200) # Name of the region (e.g., 'Hippocampal formation')
region_abbr=NULL: varchar(64) # Standard abbreviation (e.g., 'HPF')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is having a abbreviation helpful? We already only use the region name.

subregion_name=NULL: varchar(200) # Subregion name (e.g., 'Cornu Ammonis 1')
subregion_abbr=NULL: varchar(64) # Subregion abbreviation (e.g., 'CA1')
subsubregion_name=NULL: varchar(200) # Sub-subregion name (e.g., 'stratum pyramidale')
subsubregion_abbr=NULL: varchar(64) # Sub-subregion abbreviation (e.g., 'sp')
atlas_source: varchar(128) # Source atlas (e.g., 'Allen CCF v3', 'Paxinos Rat 6th Ed')
"""

# TODO consider making (region_name, subregion_name, subsubregion_name) a
Expand Down Expand Up @@ -52,6 +56,65 @@ def fetch_add(
)
query = BrainRegion & key
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider removing this function or only allowing brain regions from a specific atlas.

if not query:
logger.info(
f"Brain region '{region_name}' not found. Adding to BrainRegion. "
"Please make sure to check the spelling and format."
"Remove any extra spaces or special characters."
)
cls.insert1(key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a meeting we discussed limiting this operation to admin

query = BrainRegion & key
return query.fetch1("region_id")


@schema
class CoordinateSystem(dj.Lookup):
definition = """
# Defines standard coordinate systems used for spatial data.
coordinate_system_id: varchar(64) # Primary key (e.g., 'Allen_CCFv3_RAS_um')
---
description: varchar(255) # Description of the coordinate system
orientation: enum( # Anatomical orientation convention
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this too flexible?

"RAS", "LPS", "PIR", "ASL", "XYZ", "Other"
)
unit: enum( # Spatial unit
"um", "mm", "pixels", "voxels"
)
atlas_source=NULL: varchar(128) # Source if based on an atlas (e.g., 'Allen CCF v3', 'WHS Rat v4')
"""
contents = [
[
"Allen_CCFv3_RAS_um",
"Allen CCF v3 Mouse Atlas, RAS orientation, micrometers",
"RAS",
"um",
"Allen CCF v3",
],
[
"Paxinos_Rat_6th_PIR_um",
"Paxinos & Watson Rat Atlas 6th Ed, PIR orientation, micrometers",
"PIR",
"um",
"Paxinos Rat 6th Ed",
],
[
"WHS_Rat_v4_RAS_um",
"Waxholm Space Sprague Dawley Rat Atlas v4, RAS orientation, micrometers",
"RAS",
"um",
"WHS Rat v4",
],
[
"Histology_Image_Pixels",
"2D Pixels from processed histology image (Origin/Orientation Varies)",
"Other",
"pixels",
None,
],
[
"MicroCT_Voxel_Scan",
"3D Voxel space from raw microCT scan (Orientation relative to scanner)",
"XYZ", # Or 'Other' depending on context
"voxels",
None,
],
]
5 changes: 5 additions & 0 deletions src/spyglass/electrode_localization/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from spyglass.electrode_localization.localization_merge import (
ChannelBrainLocation,
ChannelBrainLocationHistologyV1,
ChannelBrainLocationMicroCTV1,
)
36 changes: 36 additions & 0 deletions src/spyglass/electrode_localization/localization_merge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Merge of electrode channel locations from histology and microCT pipelines."""

import datajoint as dj

from spyglass.electrode_localization.v1.histology import ( # noqa: F401
ChannelBrainLocationHistologyV1,
)
from spyglass.electrode_localization.v1.micro_ct import ( # noqa: F401
ChannelBrainLocationMicroCTV1,
)
from spyglass.utils import SpyglassMixin
from spyglass.utils.dj_merge_tables import _Merge

schema = dj.schema("electrode_localization_v1")


@schema
class ChannelBrainLocation(_Merge, SpyglassMixin):
"""Merge of electrode channel locations from histology and microCT pipelines.

The master table lists each (subject, probe, channel), with parts:
- HistologyV1: ground‐truth coordinates from manual histology alignment.
- MicroCTV1: coregistered coordinates from the microCT volume.
"""

class HistologyV1(SpyglassMixin, dj.Part):
definition = """
-> master
-> ChannelBrainLocationHistologyV1
"""

class MicroCTV1(SpyglassMixin, dj.Part):
definition = """
-> master
-> ChannelBrainLocationMicroCTV1
"""
Empty file.
32 changes: 32 additions & 0 deletions src/spyglass/electrode_localization/v1/histology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Histology-derived coordinates and region assignment for an electrode"""

import datajoint as dj

from spyglass.common import (
BrainRegion,
CoordinateSystem,
Electrode,
) # noqa: F401
from spyglass.histology.v1.histology import ( # noqa: F401
HistologyImages,
HistologyRegistration,
)
from spyglass.utils import SpyglassMixin

schema = dj.schema("electrode_localization_v1")


@schema
class ChannelBrainLocationHistologyV1(SpyglassMixin, dj.Manual):
definition = """
# Histology-derived coordinates and region assignment for an electrode
-> Electrode # Electrode being localized
-> HistologyImages # Source NWB file link for histology images
-> HistologyRegistration # Alignment parameters used
---
-> CoordinateSystem # Defines the space for pos_x,y,z (e.g., Allen CCF RAS um)
pos_x: float # (um) coordinate in the specified space
pos_y: float # (um) coordinate in the specified space
pos_z: float # (um) coordinate in the specified space
-> BrainRegion # Assigned brain region
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nullable? Is there ever a case where this is not known, or the value retrieved doesn't make sense in this case - either a grey area or individual differences for the animal vs averaged atlas?

Is this an imported table? Is BrainRegion calculated based on the positions found?

"""
32 changes: 32 additions & 0 deletions src/spyglass/electrode_localization/v1/micro_ct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""MicroCT-derived coordinates and region assignment for an electrode"""

import datajoint as dj

from spyglass.common import (
BrainRegion,
CoordinateSystem,
Electrode,
) # noqa: F401
from spyglass.micro_ct.v1.micro_ct import ( # noqa: F401
MicroCTImages,
MicroCTRegistration,
)
from spyglass.utils import SpyglassMixin

schema = dj.schema("electrode_localization_v1")


@schema
class ChannelBrainLocationMicroCTV1(SpyglassMixin, dj.Manual):
definition = """
# MicroCT-derived coordinates and region assignment for an electrode
-> Electrode # Electrode being localized
-> MicroCTImages # Source NWB file link for microCT data
-> MicroCTRegistration # Alignment parameters used
---
-> CoordinateSystem # Defines the space for pos_x,y,z
pos_x: float # (um) coordinate in the specified space
pos_y: float # (um) coordinate in the specified space
pos_z: float # (um) coordinate in the specified space
-> BrainRegion # Assigned brain region
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same questions as histology table above

"""
Empty file.
Empty file.
141 changes: 141 additions & 0 deletions src/spyglass/histology/v1/histology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import datajoint as dj

from spyglass.common import ( # noqa: F401
AnalysisNwbfile,
CoordinateSystem,
LabMember,
Subject,
)
from spyglass.utils import SpyglassMixin, logger

schema = dj.schema("histology_v1")


@schema
class Histology(SpyglassMixin, dj.Manual):
definition = """
# Represents a single histology preparation for a subject
Comment on lines +16 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
definition = """
# Represents a single histology preparation for a subject
definition = # A single histology preparation for a subject

-> Subject
histology_id: varchar(32) # User-defined ID (e.g., 'probe_track_run1')
---
prep_date=NULL: date # Optional: Date of tissue preparation
slice_orientation: enum( # Orientation of sections
"coronal",
"sagittal",
"horizontal",
"other"
)
slice_thickness: float # (um) Thickness of sections
mounting_medium=NULL: varchar(128) # e.g., 'DPX', 'Fluoromount-G'
experiment_purpose: varchar(1024) # e.g., 'Probe track recovery', 'Anatomical ref'
notes="": varchar(2048) # Optional general notes about the preparation
-> [nullable] LabMember.proj(histology_experimenter='user_name') # Optional: who did the prep?
# --- Data Source ---
output_format='TIFF stack': varchar(64) # Format of raw image data from scanner
raw_scan_path: varchar(512) # Path to the raw output (e.g., folder containing TIFF stack)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are user's intended to access the images from this path or the AnalysisNwbfile? If the latter, we might not need to store it in the table, but rather pass it to an insertion method

"""

class HistologyStain(SpyglassMixin, dj.Part):
definition = """
# Details of specific stains used in a histology preparation
-> Histology
stain_index: tinyint unsigned # Use index for multiple stains (0, 1, 2...)
---
identified_feature: varchar(128) # Biological target/marker (e.g., 'Nissl Bodies', 'ChR2-tdTomato+', 'ProbeTrack_DiI')
visualization_agent: varchar(128)# Method/molecule making feature visible (e.g., 'Cresyl Violet', 'Native tdTomato', 'DiI', 'Alexa 488')
stain_type: enum( # Type of staining method used
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this list exhaustive? Would we want to make this a varchar for flexibility?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're not sure, then yes, I'd convert

"immunohistochemistry",
"genetic_marker",
"tracer",
"anatomical",
"histochemical",
"in_situ_hybridization",
"other"
) = "other"
stain_protocol_name=NULL: varchar(128) # Optional: name of the protocol used
antibody_details=NULL: varchar(255) # Optional: specific antibody info (e.g. company, cat#, lot#)
stain_notes="": varchar(1024) # Optional notes about this specific stain (e.g., concentration)
"""


@schema
class HistologyImages(SpyglassMixin, dj.Computed):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are multiple HistologyImage for one Histology entry, would it make more sense to make this a part table of Histology?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CBroz1 do you have an opinion on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes? It would be helpful for me to talk through a use case, but yes, moving to a part table seems right

What happens to these images downstream? Is some analysis just going to look at the brain region or electrode coordinates and ignore image-specific info? If so, if no downstream table cares about specific images, then we might be better off just including image-specific info in blobs/nwbs that we're unlikely to fetch - for the sake of streamlining primary keys and cutting down on the table count

The current approach seems low table count but high information. I could either see us increasing the table count to better accommodate the same info, or reducing the info for the v1 version of this pipeline and conceding that the info can be dumped in less-accessible places for the sake of pipeline streamlining

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently these images aren't used for electrode localization, but they could be in the future. I guess it depends on how much we want to worry about this in v1.

definition = """
# Links Histology info to the Analysis NWB file containing the image data
-> Histology
images_id: varchar(32) # User-defined ID for these images (e.g., histology_id)
---
-> AnalysisNwbfile # Link to the NWB file storing image data
processing_time=CURRENT_TIMESTAMP: timestamp # Timestamp of NWB file creation
# --- Image Acquisition/Processing Details ---
color_to_stain=NULL: blob # Mapping channel colors to stain features (e.g., {'DAPI': 'blue', 'GFAP': 'green'})
pixel_size_x: float # (um) Pixel size in X dimension after processing/scaling
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that the post-processing pixel size will differ across images in a stack?

pixel_size_y: float # (um) Pixel size in Y dimension after processing/scaling
pixel_size_z: float # (um) Pixel size in Z dimension (often slice_thickness or scan step)
objective_magnification: float # Magnification of the objective lens (e.g., 20 for 20x)
image_modality: enum( # Modality of the microscopy
"fluorescence",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this list exhaustive? Would we want to make this a varchar for flexibility?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: if unsure, then convert

"brightfield",
"other"
)
processing_notes="": varchar(1024) # Notes on image processing applied before/during NWB creation
"""

# Ensure this key is unique for HistologyImages entries
# key_source = Histology
Copy link

Copilot AI Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If uniqueness for HistologyImages entries is required, consider uncommenting or implementing the key_source assignment.

Suggested change
# key_source = Histology
key_source = Histology

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key_source will change the make. To make unique in the table, we can add to the table definition:
unique index (subject_id, histology_id)


def make(self, key: dict) -> None:
"""
Populate HistologyImages table.
This method should:
1. Find the raw image data using `raw_scan_path` from the `Histology` table.
2. Process the images as needed (e.g., stitching, scaling).
3. Create an NWB file containing the processed image stack (e.g., using `pynwb.image.ImageSeries`).
- Store relevant metadata (pixel sizes, objective, modality, etc.) within the NWB file.
4. Create an `AnalysisNwbfile` entry for the new NWB file.
5. Insert the key, including the `analysis_file_name` from AnalysisNwbfile,
along with image metadata like `pixel_size_*`, `color_to_stain`, etc., into this table.
"""
logger.info(f"Populating HistologyImages for key: {key}")
# Placeholder: Replace with actual NWB creation and insertion logic
# Example steps (conceptual):
# 1. histology_entry = (Histology & key).fetch1()
# 2. raw_path = histology_entry['raw_scan_path']
# 3. image_data, metadata = process_histology_images(raw_path) # Your function
# 4. nwb_file_name = f"{key['subject_id']}_{key['histology_id']}_images.nwb"
# 5. nwb_file_path = AnalysisNwbfile().create(nwb_file_name)
# 6. create_histology_nwb(nwb_file_path, image_data, metadata) # Your function
# 7. AnalysisNwbfile().add(key['subject_id'], nwb_file_name)
# 8. self.insert1({
# **key,
# 'images_id': key['histology_id'], # Or generate a new unique ID if needed
# 'analysis_file_name': nwb_file_name,
# 'pixel_size_x': metadata['pixel_size_x'],
# # ... other metadata fields ...
# })
pass
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should the analysisNWB file be made? We usually have this correspond to subject + date but this would be per subject.



@schema
class HistologyRegistration(SpyglassMixin, dj.Manual):
definition = """
# Stores results/params of aligning histology image data to a target coordinate system
-> HistologyImages # Link to the source histology NWB file info
registration_id: varchar(32) # Unique ID for this registration instance/parameters
---
-> CoordinateSystem # The TARGET coordinate system (e.g., 'allen_ccf_v3_ras_um')
# --- Registration Parameters ---
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider either ...

  1. making these part tables with only -> master as the primary key. At the cost of violating the one-to-many assumption of master-parts, you get fewer fields and the ability to shorten names (HistologyRegistration.registration_X -> HistologyRegistration.Params.X)
  2. making the params a lookup table

registration_method: varchar(128) # Algorithmic approach (e.g. 'affine+bspline', 'manual_landmark')
registration_software: varchar(128) # Software used (e.g. 'ANTs', 'elastix', 'SimpleITK', 'CloudCompare')
registration_software_version: varchar(64) # Software version (e.g. '2.3.5', '5.0.1')
registration_params=NULL: blob # Store detailed parameters (e.g., dict/JSON/YAML content)
# --- Registration Results ---
transformation_matrix=NULL: blob # Store affine matrix (e.g., 4x4 np.array.tobytes())
warp_field_path=NULL: varchar(512) # Path to non-linear warp field file (e.g., .nii.gz, .mha)
registration_quality=NULL: float # Optional QC metric (e.g., Dice score, landmark error in um)
registration_time=CURRENT_TIMESTAMP: timestamp # Time this registration entry was created/run
registration_notes="": varchar(2048) # Specific notes about this registration run
"""
Empty file.
Empty file.
Loading
Loading