{ "cells": [ { "cell_type": "markdown", "id": "633f7334", "metadata": {}, "source": [ "# NF1 3D pilot cropped voxel view\n", "\n", "View real 3D single-cell crops (with mask overlays) from two NF1 wells in one `CytoDataFrame`.\n", "\n", "Most of the setup below isn't about `CytoDataFrame` -- it's working around this\n", "data's layout, which doesn't look like typical CellProfiler output:\n", "\n", "- There are no `Image_FileName_*`/`Image_PathName_*` columns, so each row's raw\n", " image path is built from the patient/well/field metadata (`load_image_set`).\n", "- Every well's mask file has the exact same generic name (`nuclei_mask.tiff`),\n", " so combining two wells into one table needs `stage_mask` to tell them apart\n", " -- a single-well notebook wouldn't need this at all.\n", "\n", "The actual `CytoDataFrame(...)` call at the end is just a handful of arguments." ] }, { "cell_type": "code", "execution_count": 1, "id": "7967ce3f", "metadata": { "execution": { "iopub.execute_input": "2026-09-22T22:35:54.865756Z", "iopub.status.busy": "2026-09-22T22:35:54.865377Z", "iopub.status.idle": "2026-09-22T22:35:55.409082Z", "shell.execute_reply": "2026-09-22T22:35:55.408497Z" } }, "outputs": [], "source": [ "import os\n", "import pathlib\n", "import re\n", "import tempfile\n", "\n", "import pandas as pd\n", "\n", "from cytodataframe import CytoDataFrame\n", "\n", "# Local mount point for bandicoot. Set BANDICOOT_LOCAL_BASE if it is mounted\n", "# somewhere else on your machine.\n", "BANDICOOT_LOCAL_BASE = pathlib.Path(\n", " os.environ.get(\"BANDICOOT_LOCAL_BASE\", \"~/mnt/bandicoot\")\n", ").expanduser()\n", "DATA_DIR = BANDICOOT_LOCAL_BASE / \"NF1_organoid_data/data\"\n", "PROFILES_DIR = (\n", " DATA_DIR / \"image_based_profiles_production_zedprofiler/ibp/sc_profiles_related\"\n", ")\n", "if not PROFILES_DIR.is_dir():\n", " raise FileNotFoundError(\n", " f\"Profiles not found at {PROFILES_DIR}. Set the BANDICOOT_LOCAL_BASE \"\n", " \"environment variable to wherever bandicoot is mounted on this machine.\"\n", " )\n", "\n", "# An \"image set\" here is one well + field-of-view (one 3D image). Add every\n", "# image set you want to view to this list -- e.g. all wells/fields in a\n", "# plate for per-plate QC -- not just the two shown here.\n", "IMAGE_SETS = [\n", " \"NF0055_T1__NF0055_T1__B10__F1\",\n", " \"NF0014_T1__NF0014_T1__C4__F2\",\n", "]\n", "CHANNEL = \"DNA\"\n", "CHANNEL_CODE = \"405\"\n", "COMPARTMENT = \"Nuclei\"\n", "\n", "# Every well's mask file has the same generic name (e.g. \"nuclei_mask.tiff\"),\n", "# but CytoDataFrame's data_mask_context_dir only matches masks by filename\n", "# pattern within one shared directory -- it can't tell two identically-named\n", "# files in different wells apart. MASK_LINK_DIR is a scratch directory (not\n", "# part of the repo) that `stage_mask` below fills with per-well symlinks to\n", "# the real mask files, renamed to embed each well's own identifier, so that\n", "# matching works.\n", "MASK_LINK_DIR = pathlib.Path(tempfile.gettempdir()) / \"cytodataframe_nf1_3d_mask_links\"\n", "MASK_LINK_DIR.mkdir(exist_ok=True)\n", "\n", "\n", "def load_image_set(image_set: str) -> tuple[pd.DataFrame, pathlib.Path, pathlib.Path]:\n", " profiles = pd.read_parquet(PROFILES_DIR / f\"{image_set}.parquet\").head(1)\n", " row = profiles.iloc[0]\n", " patient = row[\"Metadata_Biology_PatientTumor\"]\n", " # e.g. B10-1\n", " well_field = (\n", " f\"{row['Metadata_Experiment_WellID']}-{row['Metadata_Imaging_FieldID']}\"\n", " )\n", "\n", " channel_path = (\n", " DATA_DIR\n", " / patient\n", " / \"zstack_images\"\n", " / well_field\n", " / f\"{well_field}_{CHANNEL_CODE}.tif\"\n", " )\n", " mask_path = (\n", " DATA_DIR\n", " / patient\n", " / \"segmentation_masks\"\n", " / well_field\n", " / f\"{COMPARTMENT.lower()}_mask.tiff\"\n", " )\n", " profiles[f\"Image_FileName_{CHANNEL}\"] = str(channel_path)\n", " return profiles, channel_path, mask_path\n", "\n", "\n", "def stage_mask(channel_path: pathlib.Path, mask_path: pathlib.Path) -> pathlib.Path:\n", " # See MASK_LINK_DIR above for why this exists.\n", " link = MASK_LINK_DIR / f\"{channel_path.stem}__{mask_path.name}\"\n", " if not link.exists():\n", " link.symlink_to(mask_path)\n", " return link" ] }, { "cell_type": "markdown", "id": "cad4d181", "metadata": {}, "source": [ "Load one object per well into a single two-row table." ] }, { "cell_type": "code", "execution_count": 2, "id": "ee55b730", "metadata": { "execution": { "iopub.execute_input": "2026-09-22T22:35:55.410551Z", "iopub.status.busy": "2026-09-22T22:35:55.410439Z", "iopub.status.idle": "2026-09-22T22:35:56.906940Z", "shell.execute_reply": "2026-09-22T22:35:56.905833Z" } }, "outputs": [], "source": [ "profile_rows = []\n", "for image_set in IMAGE_SETS:\n", " profiles, channel_path, mask_path = load_image_set(image_set)\n", " stage_mask(channel_path, mask_path)\n", " profile_rows.append(profiles)\n", "\n", "profiles = pd.concat(profile_rows, ignore_index=True)\n", "mask_name = mask_path.name" ] }, { "cell_type": "code", "execution_count": 3, "id": "f8431d7c", "metadata": { "execution": { "iopub.execute_input": "2026-09-22T22:35:56.910234Z", "iopub.status.busy": "2026-09-22T22:35:56.909395Z", "iopub.status.idle": "2026-09-22T22:44:25.572399Z", "shell.execute_reply": "2026-09-22T22:44:25.570369Z" } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "4f7a043f9747408581b92b1235074112", "version_major": 2, "version_minor": 0 }, "text/plain": [ "GridspecLayout(children=(HTML(value=\"