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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ images or raw intensity data through the analysis pipeline.
| Uploaded ND2 files | `backend/app/nd2files/` |
| Cell databases | `backend/app/databases/<nd2_stem>.db` |
| Extracted contour previews | `backend/app/extracted_data/<nd2_stem>/<frame>.png` |
| Cell table | SQLite table `cells`, including `cell_id`, `manual_label`, `perimeter`, `area`, `img_ph`, `img_fluo1`, `img_fluo2`, `contour`, center coordinates, objective, and pixel size. |
| Cell table | SQLite table `cells`, including `cell_id`, `manual_label`, `perimeter`, `area`, `img_ph`, `img_fluo1`, `img_fluo2`, `contour`, crop-local center coordinates, original ND2 frame position coordinates, objective, and pixel size. |
| Bulk exports | JSON/CSV/PNG responses from Bulk Engine endpoints or browser downloads. |
| Frontend production build | `frontend/dist/`; the backend serves this folder when it exists. |

Expand Down
4 changes: 2 additions & 2 deletions backend/app/cellextraction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ Failure response:
- Databases: SQLite files written to `backend/app/databases/`.
- Default name: `<nd2_stem>.db` with dots in the filename replaced by `p`.
- Table: `cells` (stores `cell_id`, `perimeter`, `area`, `img_ph`, `img_fluo1`,
`img_fluo2`, `contour`, `center_x`, `center_y`, and labels).
- With `auto_annotation=true`, `manual_label` is initialized to `1` or `N/A`.
`img_fluo2`, `contour`, crop-local `center_x`/`center_y`, original ND2
frame `position_x`/`position_y`, and labels).
- Contour overlays: PNGs written to `backend/app/extracted_data/<nd2_stem>/`.
- Files are named by frame index, e.g. `0.png`, `1.png`, `2.png`.

Expand Down
64 changes: 60 additions & 4 deletions backend/app/cellextraction/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pickle
import random
import re
import json
import logging
from functools import lru_cache
from dataclasses import dataclass
Expand Down Expand Up @@ -35,6 +36,7 @@
DATABASES_DIR: Path = APP_DIR / "databases"
EXTRACTED_DATA_DIR: Path = APP_DIR / "extracted_data"
TEMPDATA_DIR: Path = APP_DIR / "tempdata"
CELL_POSITIONS_FILENAME: str = "cell_positions.json"
AUTOANNOTATION_MODEL_PATH: Path = (
APP_DIR.parent / "autoannotation" / "artifacts" / "autoannotator.pkl"
)
Expand Down Expand Up @@ -302,6 +304,8 @@ class Cell(Base):
contour = Column(BLOB)
center_x = Column(FLOAT)
center_y = Column(FLOAT)
position_x = Column(FLOAT, nullable=True)
position_y = Column(FLOAT, nullable=True)
user_id = Column(String, nullable=True)
objective_magnification = Column(String, nullable=True)
pixel_size_um = Column(FLOAT, nullable=True)
Expand Down Expand Up @@ -681,6 +685,7 @@ def _ensure_dir(path: str) -> None:
_ensure_dir(path)
loop_num = num_tiff // set_num if mode != "single_layer" else num_tiff
for k in range(loop_num):
frame_dir = f"{temp_dir}/frames/tiff_{k}"
image_ph_raw = SyncChores.load_image_unchanged(f"{temp_dir}/PH/{k}.tif")
if image_ph_raw is None:
continue
Expand Down Expand Up @@ -740,9 +745,10 @@ def _ensure_dir(path: str) -> None:
cv2.drawContours(image_ph_copy, contours, -1, (0, 255, 0), 3)
cv2.imwrite(f"{contour_dir}/{k}.png", image_ph_copy)
n = 0
saved_positions: dict[str, dict[str, float]] = {}
if mode in ("triple_layer", "quad_layer"):
for ph, fluo1, fluo2 in zip(
cropped_images_ph, cropped_images_fluo_1, cropped_images_fluo_2
for candidate_index, (ph, fluo1, fluo2) in enumerate(
zip(cropped_images_ph, cropped_images_fluo_1, cropped_images_fluo_2)
):
if (
len(ph) == output_size[0]
Expand All @@ -759,21 +765,41 @@ def _ensure_dir(path: str) -> None:
cv2.imwrite(
f"{temp_dir}/frames/tiff_{k}/Cells/fluo2/{n}.png", fluo2
)
cx, cy = contour_centers[candidate_index]
saved_positions[str(n)] = {
"position_x": float(cx),
"position_y": float(cy),
}
n += 1

elif mode == "single_layer":
for ph in cropped_images_ph:
for candidate_index, ph in enumerate(cropped_images_ph):
if len(ph) == output_size[0] and len(ph[0]) == output_size[1]:
cv2.imwrite(f"{temp_dir}/frames/tiff_{k}/Cells/ph/{n}.png", ph)
cx, cy = contour_centers[candidate_index]
saved_positions[str(n)] = {
"position_x": float(cx),
"position_y": float(cy),
}
n += 1
elif mode == "dual_layer":
for ph, fluo1 in zip(cropped_images_ph, cropped_images_fluo_1):
for candidate_index, (ph, fluo1) in enumerate(
zip(cropped_images_ph, cropped_images_fluo_1)
):
if len(ph) == output_size[0] and len(ph[0]) == output_size[1]:
cv2.imwrite(f"{temp_dir}/frames/tiff_{k}/Cells/ph/{n}.png", ph)
cv2.imwrite(
f"{temp_dir}/frames/tiff_{k}/Cells/fluo1/{n}.png", fluo1
)
cx, cy = contour_centers[candidate_index]
saved_positions[str(n)] = {
"position_x": float(cx),
"position_y": float(cy),
}
n += 1
positions_path = Path(frame_dir) / "Cells" / CELL_POSITIONS_FILENAME
with positions_path.open("w", encoding="utf-8") as handle:
json.dump(saved_positions, handle, ensure_ascii=True, indent=2)
return num_tiff


Expand Down Expand Up @@ -848,6 +874,33 @@ def process_image(
contour = contours[0] if contours else None
return contour, img_ph_gray, img_fluo1_gray, img_fluo2_gray

def get_cell_original_position(
self,
i: int,
j: int,
) -> tuple[float | None, float | None]:
positions_path = (
Path(self.temp_dir)
/ "frames"
/ f"tiff_{i}"
/ "Cells"
/ CELL_POSITIONS_FILENAME
)
try:
with positions_path.open("r", encoding="utf-8") as handle:
positions = json.load(handle)
except (FileNotFoundError, json.JSONDecodeError, TypeError):
return None, None

entry = positions.get(str(j)) if isinstance(positions, dict) else None
if not isinstance(entry, dict):
return None, None

try:
return float(entry["position_x"]), float(entry["position_y"])
except (KeyError, TypeError, ValueError):
return None, None

def process_cell(
self,
i: int,
Expand Down Expand Up @@ -876,6 +929,7 @@ def process_cell(
or abs(center_y - img_ph.shape[0] // 2) >= 3
):
return None
position_x, position_y = self.get_cell_original_position(i, j)

img_ph_data = cv2.imencode(".png", img_ph_gray)[1].tobytes()
img_fluo1_data = img_fluo2_data = None
Expand Down Expand Up @@ -914,6 +968,8 @@ def process_cell(
contour=contour_blob,
center_x=center_x,
center_y=center_y,
position_x=position_x,
position_y=position_y,
user_id=user_id,
objective_magnification=self.objective_magnification,
pixel_size_um=self.pixel_size_um,
Expand Down
Loading