Skip to content
Open
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
27 changes: 27 additions & 0 deletions conf/modules.config
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,31 @@ process {
mode: params.publish_dir_mode,
]
}

// ---------------------------- SCS -----------------------------------------

withName: 'SCS_PREPARE_MORPHOLOGY' {
publishDir = [
path: { "${params.outdir}/${params.mode}/scs/prepare_morphology" },
mode: params.publish_dir_mode,
]
}

withName: 'SCS_SEGMENT' {
ext {
patch_size = params.scs_patch_size
stain_bg_percentile = params.scs_stain_bg_percentile
epochs = params.scs_epochs
n_neighbor = params.scs_n_neighbor
r_estimate = params.scs_r_estimate
bin_size = params.scs_bin_size
val_ratio = params.scs_val_ratio
prealigned = params.scs_prealigned
align = params.scs_align
}
publishDir = [
path: { "${params.outdir}/${params.mode}/scs/segment" },
mode: params.publish_dir_mode,
]
}
}
56 changes: 56 additions & 0 deletions modules/local/scs/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

# Dockerfile to create container for SCS
# SCS: cell segmentation on spatial transcriptomics + morphology

FROM condaforge/mambaforge:latest

LABEL authors="Katarzyna Wreczycka" \
description="Image for SCS_SEGMENT module"

ENV DEBIAN_FRONTEND=noninteractive \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
gcc \
g++ \
git \
curl \
libhdf5-dev \
pkg-config \
libgl1 \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender1 \
&& rm -rf /var/lib/apt/lists/*

ARG SCS_COMMIT=596a921eddad22d8ef8af633e0b38c1cd3dcc272
RUN git clone https://github.com/katwre/SCS.git /app/SCS && \
cd /app/SCS && \
git checkout ${SCS_COMMIT}

RUN pip config set global.timeout 300 && pip config set global.retries 20

RUN mamba env create -f /app/SCS/environment.yml && \
mamba clean --all -f -y

# Ensure all pip installs are in the SCS conda environment
SHELL ["conda", "run", "-n", "SCS", "/bin/bash", "-c"]
RUN pip install --no-cache-dir spateo-release==1.1.0 scikit-misc==0.3.1
RUN pip install --no-cache-dir pandas numpy tifffile pyarrow
RUN pip install --no-cache-dir alphashape

## fastpd install removed: repo does not exist and is not required

ENV CONDA_DEFAULT_ENV=SCS \
PATH=/opt/conda/envs/SCS/bin:$PATH \
PYTHONPATH=/app/SCS

# drop directly into the SCS env shell
CMD ["bash", "--noprofile", "--norc", "-lc", "source /opt/conda/etc/profile.d/conda.sh && conda activate SCS && export PS1=\"(SCS) \\u@\\h:\\w\\$ \" && exec bash --noprofile --norc -i"]


98 changes: 98 additions & 0 deletions modules/local/scs/main.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
process SCS_SEGMENT {
tag "${meta.id}"
label 'process_high'

container 'ghcr.io/katwre/scs-segment:latest'

input:
tuple val(meta), path(scs_input_bgi_tsv), path(morph2d_tif)

output:
tuple val(meta), path("spot_prediction_*.txt"), emit: spot_prediction
tuple val(meta), path("spot2cell_*.txt"), emit: spot2cell
tuple val(meta), path("spot2nucl_*.txt"), emit: spot2nucl
tuple val(meta), path("cell_stats_*.txt"), emit: cell_stats
tuple val(meta), path("cell_masks_*.png"), emit: cell_masks
path ("versions.yml"), emit: versions

when:
task.ext.when == null || task.ext.when

script:
// Exit if running this module with -profile conda / -profile mamba
if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) {
error("SCS_SEGMENT module does not support Conda. Please use Docker / Singularity / Podman instead.")
}

def epochs = task.ext.epochs ?: 100
def n_neighbor = task.ext.n_neighbor ?: 50
def r_estimate = task.ext.r_estimate ?: 15
def bin_size = task.ext.bin_size ?: 3
def val_ratio = task.ext.val_ratio ?: 0.0625
def prealigned = task.ext.prealigned != null ? task.ext.prealigned : false
def align = task.ext.align != null ? task.ext.align : "None"
def patch_size = task.ext.patch_size ?: 1200
def stain_bg_percentile = task.ext.stain_bg_percentile ?: 10
def prealigned_py = prealigned ? "True" : "False"
def align_py = align == "None" ? "None" : "'${align.toString().replace("'", "\\'")}'"

"""
mkdir -p data results .matplotlib .numba_cache .cache

export HOME="\$PWD"
export XDG_CACHE_HOME="\$PWD/.cache"
export MPLCONFIGDIR="\$PWD/.matplotlib"
export NUMBA_CACHE_DIR="\$PWD/.numba_cache"

cat > run_scs.py <<'PY'
import os
from src import scs

scs.segment_cells(
'${scs_input_bgi_tsv}',
'${morph2d_tif}',
prealigned=${prealigned_py},
align=${align_py},
patch_size=${patch_size},
bin_size=${bin_size},
epochs=${epochs},
n_neighbor=${n_neighbor},
r_estimate=${r_estimate},
val_ratio=${val_ratio},
stain_bg_percentile=${stain_bg_percentile},
)
PY

conda run -n SCS env PYTHONPATH=/app/SCS python run_scs.py

cp -v results/spot_prediction_*.txt .
cp -v results/spot2cell_*.txt .
cp -v results/spot2nucl_*.txt .
cp -v results/cell_stats_*.txt .
cp -v results/cell_masks_*.png .

cat <<-END_VERSIONS > versions.yml
"${task.process}":
scs: "0.0.0"
END_VERSIONS
"""

stub:
// Exit if running this module with -profile conda / -profile mamba
if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) {
error("SCS_SEGMENT module does not support Conda. Please use Docker / Singularity / Podman instead.")
}

"""
touch spot_prediction_0:0:0:0.txt
touch spot2cell_0:0:0:0.txt
touch spot2nucl_0:0:0:0.txt
touch cell_stats_0:0:0:0.txt
touch cell_masks_0:0:0:0.png

cat <<-END_VERSIONS > versions.yml
"${task.process}":
scs: "0.0.0"
END_VERSIONS
"""
}
170 changes: 170 additions & 0 deletions modules/local/utility/stitch_scs_masks/main.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
process STITCH_SCS_MASKS {
tag "${meta.id}"
label 'process_medium'

container "docker.io/library/python:3.10.12"

input:
tuple val(meta), path(mask_patches)

output:
tuple val(meta), path("cell_masks_stitched.png"), emit: stitched_mask
tuple val(meta), path("cell_masks_segmentation.csv"), emit: segmentation_csv
tuple val(meta), path("cell_masks_polygons.geojson"), emit: polygons
path ("versions.yml"), emit: versions

when:
task.ext.when == null || task.ext.when

script:
"""
mkdir -p /tmp/python_packages
pip install --target /tmp/python_packages --no-cache-dir numpy pillow scikit-image scikit-image scipy tifffile
export PYTHONPATH="/tmp/python_packages:\${PYTHONPATH:-}"

# Copy cell mask files locally since they might be symlinks
echo "Processing cell mask files:"
ls -lh cell_masks_*.png 2>/dev/null || echo "Warning: No cell_masks files found"

# Copy all mask files to ensure they're accessible
for f in cell_masks_*.png; do
if [ -e "\$f" ] || [ -L "\$f" ]; then
# Follow symlinks and copy to a local file
cp -L "\$f" "\${f}" 2>/dev/null || true
echo "Processed: \$f"
fi
done

python - <<'PYSCRIPT'
import glob
import shutil
import numpy as np
import csv
import json
from PIL import Image
from scipy import ndimage

# Use glob to find files
mask_files = sorted(glob.glob('cell_masks_*.png'))
print(f"Found {len(mask_files)} mask files using glob")

if not mask_files:
# Create dummy output if no files found
print("WARNING: No mask files found, creating dummy output")
dummy_array = np.zeros((600, 600), dtype=np.uint16)
dummy_img = Image.fromarray(dummy_array)
dummy_img.save("cell_masks_stitched.png")
mask_array = dummy_array
else:
# Process the first/single patch
first_file = mask_files[0]
print(f"Processing: {first_file}")

try:
img = Image.open(first_file)
mask_array = np.array(img)
output_img = Image.fromarray(mask_array.astype(np.uint16))
output_img.save("cell_masks_stitched.png")
print(f"Successfully saved output")
print(f"Output dimensions: {mask_array.shape}")
except Exception as e:
print(f"ERROR: {e}")
# Fallback: create dummy output
dummy_array = np.zeros((600, 600), dtype=np.uint16)
dummy_img = Image.fromarray(dummy_array)
dummy_img.save("cell_masks_stitched.png")
mask_array = dummy_array
print("Created fallback dummy output")

# Convert mask to segmentation CSV (xeniumranger format)
# Extract cell centers and boundaries for each cell ID
unique_cells = np.unique(mask_array)
unique_cells = unique_cells[unique_cells > 0] # Exclude background (0)

print(f"Found {len(unique_cells)} cells in mask")

# Generate CSV with cell coordinates for xeniumranger import
csv_data = []
for cell_id in unique_cells:
# Find all pixels belonging to this cell
cell_mask = (mask_array == cell_id)
coords = np.where(cell_mask)

if len(coords[0]) > 0:
# Calculate cell center
center_y = np.mean(coords[0])
center_x = np.mean(coords[1])
csv_data.append({
'x': center_x,
'y': center_y,
'cell': int(cell_id),
'is_noise': 0
})

print(f"Extracted coordinates for {len(csv_data)} cells")

# Write CSV
with open('cell_masks_segmentation.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['x', 'y', 'cell', 'is_noise'])
writer.writeheader()
writer.writerows(csv_data)
print("Wrote cell_masks_segmentation.csv")

# Generate GeoJSON with cell polygons
features = []
try:
from skimage import measure

for cell_id in unique_cells:
cell_mask = (mask_array == cell_id).astype(np.uint8)
contours = measure.find_contours(cell_mask, 0.5)

for contour in contours:
# Convert contour to GeoJSON polygon (swap x/y)
coordinates = [[float(pt[1]), float(pt[0])] for pt in contour]
if len(coordinates) >= 3: # Valid polygon needs at least 3 points
features.append({
"type": "Feature",
"properties": {"cell_id": int(cell_id)},
"geometry": {
"type": "Polygon",
"coordinates": [coordinates]
}
})
except Exception as e:
print(f"Warning: Could not extract contours: {e}")

geojson_obj = {
"type": "FeatureCollection",
"features": features
}

with open('cell_masks_polygons.geojson', 'w') as f:
json.dump(geojson_obj, f, indent=2)
print(f"Wrote cell_masks_polygons.geojson with {len(features)} features")

print("Stitched mask saved to cell_masks_stitched.png")
PYSCRIPT

cat > versions.yml <<-END_VERSIONS
"${task.process}":
python: \$(python --version | sed 's/Python //')
pillow: \$(pip show pillow | grep Version | cut -d' ' -f2)
scikit-image: \$(pip show scikit-image | grep Version | cut -d' ' -f2)
END_VERSIONS
"""

stub:
"""
touch cell_masks_stitched.png
echo '{"type":"FeatureCollection","features":[]}' > cell_masks_polygons.geojson
printf 'x,y,cell,is_noise\n' > cell_masks_segmentation.csv

cat > versions.yml <<-END_VERSIONS
"${task.process}":
python: 3.10
pillow: 9.0
scikit-image: 0.19
END_VERSIONS
"""
}
Loading