Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Git
.git
.gitignore
.github

# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
*.egg-info
dist
build
.venv
venv
env
ENV

# Testing
.pytest_cache
.coverage
htmlcov
.tox
.mypy_cache
.ruff_cache

# Documentation
site
docs/requirements.txt

# IDE
.vscode
.idea
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Project specific
scripts/*.ipynb
scripts/*.nii
scripts/*.tiff
scripts/*.jpeg
scripts/*.yaml
tests/data

# Poetry
poetry.lock.bak

2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ jobs:
run: poetry install --with dev --extras all
- name: Run the automated tests
run: poetry run pytest -v
working-directory: ./tests
working-directory: tests
66 changes: 66 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Build and Push Docker Image

on:
push:
branches:
- main
- master
tags:
- 'v*'
pull_request:
branches:
- main
- master
workflow_dispatch:

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'pull_request' }}
type=sha,enable=${{ github.event_name == 'pull_request' }}
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

62 changes: 62 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# ----------------------------------------------------------------------------
# STAGE 1: Builder
# Used to install Poetry, compile dependencies, and create the virtual environment.
# ----------------------------------------------------------------------------
FROM python:3.11-slim AS builder

# Set working directory
WORKDIR /app

# Install build dependencies (GCC, etc.)
# We need these to compile packages, but we don't want them in the final image.
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*

# Install Poetry
RUN pip install poetry==1.8.4

# Configure Poetry to create the venv inside the project folder
# This makes it easy to copy the whole folder to the next stage
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache

# Copy dependency definition files first (for caching)
COPY pyproject.toml poetry.lock ./
RUN poetry lock
# Install dependencies (only the libraries)
RUN poetry install --no-root --no-dev --extras all && rm -rf $POETRY_CACHE_DIR

# Copy the rest of the application code
COPY . .
RUN poetry lock
# Install the application itself
#RUN poetry install --no-dev --extras all
RUN poetry build -f wheel
RUN ./.venv/bin/pip install dist/*.whl
# ----------------------------------------------------------------------------
# STAGE 2: Runtime
# A fresh, empty image that only contains the Python runtime and our copied venv.
# ----------------------------------------------------------------------------
FROM python:3.11-slim AS runtime

WORKDIR /app

# Create a non-root user for security (Best Practice)
RUN useradd -m -r appuser && chown appuser /app
USER appuser

# Copy the virtual environment from the builder stage
# We assume the path structure is identical (/app/.venv)
COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv

# Update PATH environment variable
# This allows us to type 'linc-convert' instead of '/app/.venv/bin/linc-convert'
ENV PATH="/app/.venv/bin:$PATH"
LABEL org.opencontainers.image.source=https://github.com/lincbrain/linc-convert
# Set the entrypoint
ENTRYPOINT ["linc-convert"]
# Default command
CMD ["--help"]
20 changes: 18 additions & 2 deletions linc_convert/modalities/lsm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,24 @@
try:
import tifffile as _ # noqa: F401

__all__ = ["cli", "mosaic", "multi_slice", "spool"]
__all__ = [
"cli",
"mosaic",
"multi_slice",
"spool",
"single_volume",
"strip",
"stitch_strips"
]

from . import cli, mosaic, multi_slice, spool
from . import (
cli,
mosaic,
multi_slice,
single_volume,
spool,
stitch_strips,
strip,
)
except ImportError:
pass
1 change: 1 addition & 0 deletions linc_convert/modalities/lsm/mosaic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Example input files can be found at
https://lincbrain.org/dandiset/000004/0.240319.1924/files?location=derivatives%2F
"""

import logging

# stdlib
Expand Down
3 changes: 2 additions & 1 deletion linc_convert/modalities/lsm/multi_slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Example input files can be found at
https://lincbrain.org/dandiset/000010/draft/files?location=sourcedata%2Fderivatives
"""

import logging

# stdlib
Expand Down Expand Up @@ -45,7 +46,7 @@ def convert(
) -> None:
"""
Convert a collection of tiff files generated by the LSM pipeline into a Zarr.

Parameters
----------
inp
Expand Down
94 changes: 94 additions & 0 deletions linc_convert/modalities/lsm/single_volume.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Converts TIFF to OME-Zarr or NIfTI-Zarr."""

import logging
import os

import cyclopts
import dask.array as da
import dask_image.imread
import numpy as np

from linc_convert.modalities.lsm.cli import lsm
from linc_convert.utils.io.zarr import from_config
from linc_convert.utils.nifti_header import build_nifti_header
from linc_convert.utils.unit import to_ome_unit
from linc_convert.utils.zarr_config import (
GeneralConfig,
NiftiConfig,
ZarrConfig,
autoconfig,
)

logger = logging.getLogger(__name__)
single_volume = cyclopts.App(name="single_volume", help_format="markdown")
lsm.command(single_volume)


@single_volume.default
@autoconfig
def convert(
inp: str,
*,
voxel_size: list[float] = (1, 1, 1),
general_config: GeneralConfig = None,
zarr_config: ZarrConfig = None,
nii_config: NiftiConfig = None,
) -> None:
"""
Tiff to OME-Zarr.

Convert tiff files
into a pyramidal OME-ZARR (or NIfTI-Zarr) hierarchy.

Parameters
----------
inp
Path to the input tiff file
voxel_size
Voxel size along the X, Y and Z dimensions, in microns.
general_config
General configuration
zarr_config
Zarr related configuration
nii_config
NIfTI header related configuration
"""
general_config.set_default_name(os.path.splitext(inp)[0])

inp_data = dask_image.imread.imread(inp)

# Prepare Zarr group
zgroup = from_config(general_config.out, zarr_config)

if not hasattr(inp_data, "dtype"):
raise Exception("Input is not a numpy array. This is unexpected.")

dataset = zgroup.create_array(
"0",
shape=inp_data.shape,
dtype=np.dtype(inp_data.dtype),
zarr_config=zarr_config,
)

if dataset.shards:
inp_data = da.rechunk(inp_data, dataset.shards)
else:
inp_data = da.rechunk(inp_data, dataset.chunks)

da.store(inp_data, dataset)
voxel_size = list(map(float, reversed(voxel_size)))
# Generate Zarr pyramid and metadata
zgroup.generate_pyramid(mode="mean", no_pyramid_axis=zarr_config.no_pyramid_axis)
logger.info("Write OME-Zarr multiscale metadata")
zgroup.write_ome_metadata(axes=["z", "y", "x"], space_unit=to_ome_unit("um"))

if nii_config.nii:
header = build_nifti_header(
zgroup=zgroup,
voxel_size_zyx=tuple(voxel_size),
unit="micrometer",
nii_config=nii_config,
)
zgroup.write_nifti_header(header)

logger.info("Conversion complete.")
2 changes: 1 addition & 1 deletion linc_convert/modalities/lsm/spool.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
https://lincbrain.org/dandiset/000010/draft/files?location=sourcedata%2Frawdata
%2Fmicr%2Fsample18_run10__y10_z01_HR&page=1
"""

import logging

# stdlib
Expand Down Expand Up @@ -233,4 +234,3 @@ def convert(
omz.write_nifti_header(header)

logger.info("Conversion complete.")

Loading
Loading