Skip to content

Commit

Permalink
- FIX: bug introduced in Vue 2.6.13 (vuejs/vue#12102).
Browse files Browse the repository at this point in the history
- WIP: `steinbock` data import.
  • Loading branch information
plankter committed Jun 7, 2021
1 parent 30df6e6 commit c7fd998
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 112 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [2.1.2] - 2021-06-07

- FIX: bug introduced in Vue 2.6.13 (https://github.com/vuejs/vue/issues/12102).
- WIP: `steinbock` data import.

## [2.1.1] - 2021-06-02

- Per-channel Min-Max / z-Score normalization as a segmentation pre-processing step.

## [2.1.0] - 2021-05-27

Initial open-source release
Expand Down
9 changes: 0 additions & 9 deletions backend/histocat/worker/io/dataset_v2.py
Expand Up @@ -123,15 +123,6 @@ def _import_cell_csv(
obs["CentroidX"] = df["Location_Center_X"]
obs["CentroidY"] = df["Location_Center_Y"]

# TODO: skip neighbors columns to keep things simple
# neighbors_cols = [col for col in df.columns if "Neighbors_" in col]
# for col in neighbors_cols:
# col_name = col.split("_")[1]
# if col_name:
# obs[col_name] = df[col]
# if col_name == "NumberOfNeighbors":
# obs[col_name] = obs[col_name].astype("int64")

var_names = []
x_df = pd.DataFrame()
for key, value in channel_order.items():
Expand Down
206 changes: 206 additions & 0 deletions backend/histocat/worker/io/steinbock.py
@@ -0,0 +1,206 @@
import logging
import os
import re
from pathlib import Path
from typing import Dict, Union, List

import anndata as ad
import pandas as pd
from sqlalchemy.orm import Session

from histocat.core.acquisition import service as acquisition_service
from histocat.core.dataset import service as dataset_service
from histocat.core.dataset.dto import DatasetCreateDto, DatasetUpdateDto
from histocat.core.dataset.models import CELL_FILENAME, DatasetModel
from histocat.core.notifier import Message
from histocat.core.project import service as project_service
from histocat.core.redis_manager import UPDATES_CHANNEL_NAME, redis_manager
from histocat.core.slide import service as slide_service
from histocat.worker.io.utils import CELL_CSV_FILENAME, IMAGE_CSV_FILENAME, copy_file

logger = logging.getLogger(__name__)


PANEL_CSV_FILE = "panel.csv"


def import_dataset(db: Session, input_folder: Path, project_id: int):
"""Import dataset from the folder compatible with 'steinbock' format."""

project = project_service.get(db, id=project_id)
if not project:
logger.warning(f"Cannot import dataset: project [id: {project_id}] does not exist.")
return

src_folder = None
for path in input_folder.rglob(PANEL_CSV_FILE):
src_folder = path.parent
break

if src_folder is None:
return

create_params = DatasetCreateDto(project_id=project_id, origin="steinbock", status="pending")
dataset = dataset_service.create(db, params=create_params)

# Metadata dictionary
meta = {}

dst_folder = Path(dataset.location)
os.makedirs(dst_folder, exist_ok=True)

# Import panel data: { Metal Tag : channel number }
channel_order = _import_panel(os.path.join(src_folder, PANEL_CSV_FILE))

masks = {}
acquisition_id_mapping = {}
for mask_file in sorted(Path(src_folder / "cell_masks").rglob("*.tiff")):
result = _import_mask(db, mask_file, dataset)
if result is not None:
mask_meta, slide_name, acquisition_origin_id = result
acquisition_id = mask_meta.get("acquisition").get("id")
image_number = mask_meta.get("acquisition").get("origin_id")
masks[acquisition_id] = mask_meta
acquisition_id_mapping[f"{slide_name}_{image_number}"] = acquisition_id
meta["masks"] = masks

regionprops_df = pd.DataFrame()
for regionprops_file in sorted(Path(src_folder / "cell_regionprops").rglob("*.csv")):
slide_name, acquisition_origin_id, df = _import_regionprops(regionprops_file, acquisition_id_mapping)
regionprops_df = regionprops_df.append(df)
regionprops_df.reset_index(inplace=True, drop=True)
regionprops_df["CellId"] = regionprops_df.index

obs = pd.DataFrame(index=regionprops_df.index)
obs["CellId"] = regionprops_df.index
obs["AcquisitionId"] = regionprops_df["AcquisitionId"]
obs["ImageNumber"] = regionprops_df["ImageNumber"]
obs["ObjectNumber"] = regionprops_df["ObjectNumber"]
obs["CentroidX"] = regionprops_df["CentroidX"]
obs["CentroidY"] = regionprops_df["CentroidY"]

print(obs)

var_names = []
x_df = pd.DataFrame()
for key, value in channel_order.items():
# TODO: check intensity multiplier
x_df[key] = range(348) # df[f"Intensity_MeanIntensity_FullStackFiltered_c{value}"] * 2 ** 16
var_names.append(key)
var = pd.DataFrame(index=var_names)
var["Channel"] = var.index
X_counts = x_df.to_numpy()

adata = ad.AnnData(X_counts, obs=obs, var=var, dtype="float32")
dst_uri = dst_folder / CELL_FILENAME
adata.write_h5ad(dst_uri)

# # Convert cell.csv to AnnData file format
# cell_df = _import_cell_csv(src_folder, dst_folder, image_number_to_acquisition_id, channel_order)

acquisition_ids = sorted(list(masks.keys()))
channels = [c[0] for c in channel_order]

update_params = DatasetUpdateDto(
name=f"Dataset {dataset.id}", status="ready", acquisition_ids=acquisition_ids, channels=channels, meta=meta
)
dataset = dataset_service.update(db, item=dataset, params=update_params)
redis_manager.publish(UPDATES_CHANNEL_NAME, Message(project_id, "dataset_imported"))


def _import_panel(path: str):
panel_df = pd.read_csv(path)
# Map Metal Tag to its order number
channel_order = dict(
[(metal_name, int(index)) for metal_name, index in zip(panel_df.channel, panel_df.index)]
)
return channel_order


def _import_mask(db: Session, filepath: Path, dataset: DatasetModel):
p = re.compile("(?P<Name>.*)_(?P<AcquisitionID>[0-9]+).tiff")
slide_name, acquisition_origin_id = p.findall(filepath.name)[0]

slide = slide_service.get_by_name(db, project_id=dataset.project_id, name=slide_name)
if slide is None:
return None
acquisition = acquisition_service.get_by_origin_id(db, slide_id=slide.id, origin_id=acquisition_origin_id)

location = copy_file(str(filepath), dataset.location)

meta = {
"location": location,
"slide": {"id": slide.id, "origin_id": slide.origin_id},
"acquisition": {"id": acquisition.id, "origin_id": acquisition.origin_id},
}
return meta, slide_name, acquisition_origin_id


def _import_regionprops(filepath: Path, acquisition_id_mapping: Dict[str, int]):
p = re.compile("(?P<Name>.*)_(?P<AcquisitionID>[0-9]+).csv")
slide_name, acquisition_origin_id = p.findall(filepath.name)[0]

df = pd.read_csv(filepath)
df.rename(columns={"Object": "ObjectNumber", "centroid-0": "CentroidY", "centroid-1": "CentroidX"}, inplace=True)
df["ImageNumber"] = acquisition_origin_id
df["AcquisitionId"] = acquisition_id_mapping.get(f"{slide_name}_{acquisition_origin_id}")
return slide_name, acquisition_origin_id, df[["ObjectNumber", "ImageNumber", "AcquisitionId", "CentroidX", "CentroidY"]]


def _import_image_csv(src_folder: Path):
src_uri = src_folder / IMAGE_CSV_FILENAME

if not src_uri.exists():
return None

df = pd.read_csv(src_uri)
return df


def _import_cell_csv(
src_folder: Path,
dst_folder: Path,
image_number_to_acquisition_id: Dict[int, int],
channel_order: Dict[str, int],
):
src_uri = src_folder / CELL_CSV_FILENAME

if not src_uri.exists():
return None

df = pd.read_csv(src_uri)
df.index = df.index.astype(str, copy=False)

obs = pd.DataFrame(index=df.index)
obs["CellId"] = df.index
obs["AcquisitionId"] = df["ImageNumber"]
obs["AcquisitionId"].replace(image_number_to_acquisition_id, inplace=True)
obs["ImageNumber"] = df["ImageNumber"]
obs["ObjectNumber"] = df["ObjectNumber"]
obs["CentroidX"] = df["Location_Center_X"]
obs["CentroidY"] = df["Location_Center_Y"]

# TODO: skip neighbors columns to keep things simple
# neighbors_cols = [col for col in df.columns if "Neighbors_" in col]
# for col in neighbors_cols:
# col_name = col.split("_")[1]
# if col_name:
# obs[col_name] = df[col]
# if col_name == "NumberOfNeighbors":
# obs[col_name] = obs[col_name].astype("int64")

var_names = []
x_df = pd.DataFrame()
for key, value in channel_order.items():
# TODO: check intensity multiplier
x_df[key] = df[f"Intensity_MeanIntensity_FullStackFiltered_c{value}"] * 2 ** 16
var_names.append(key)
var = pd.DataFrame(index=var_names)
var["Channel"] = var.index
X_counts = x_df.to_numpy()

adata = ad.AnnData(X_counts, obs=obs, var=var, dtype="float32")
dst_uri = dst_folder / CELL_FILENAME
adata.write_h5ad(dst_uri)

return df
3 changes: 3 additions & 0 deletions backend/histocat/worker/io/zip.py
Expand Up @@ -12,6 +12,7 @@
dataset_masks,
dataset_v1,
dataset_v2,
steinbock,
imcfolder,
imcfolder_v1,
mcd,
Expand Down Expand Up @@ -56,6 +57,8 @@ def import_dataset_zip(db: Session, uri: str, project_id: int):
with zipfile.ZipFile(path, "r") as zip:
zip.extractall(output_dir)

# steinbock.import_dataset(db, output_dir, project_id)

for cell_csv_filename in utils.locate(output_dir, CELL_CSV_FILENAME):
src_folder = Path(cell_csv_filename).parent
is_v2 = os.path.exists(os.path.join(src_folder, "var_cell.csv"))
Expand Down
16 changes: 8 additions & 8 deletions frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "2021.06.02",
"version": "2021.06.07",
"private": true,
"description": "histoCAT for Web",
"author": {
Expand All @@ -27,7 +27,7 @@
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"core-js": "3.13.1",
"core-js": "3.14.0",
"d3": "6.7.0",
"file-saver": "2.0.5",
"golden-layout": "2.2.1",
Expand All @@ -38,14 +38,14 @@
"register-service-worker": "1.7.2",
"regl-scatterplot": "0.19.0",
"uuid": "8.3.2",
"vue": "2.6.13",
"vue": "2.6.14",
"vue-class-component": "7.2.6",
"vue-property-decorator": "9.1.2",
"vue-router": "3.5.1",
"vuetify": "2.5.3",
"vuex": "3.6.2",
"vuex-persist": "3.1.3",
"vuex-smart-module": "0.5.0"
"vuex-smart-module": "0.6.0"
},
"devDependencies": {
"@mdi/font": "5.9.55",
Expand All @@ -67,19 +67,19 @@
"@vue/eslint-config-typescript": "7.0.0",
"@vue/test-utils": "1.2.0",
"deepmerge": "4.2.2",
"eslint": "7.27.0",
"eslint": "7.28.0",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-vue": "7.10.0",
"fibers": "5.0.0",
"jest-serializer-vue": "2.0.2",
"prettier": "2.3.0",
"prettier": "2.3.1",
"sass": "1.32.13",
"sass-loader": "10.1.1",
"sass-loader": "10.2.0",
"typescript": "4.3.2",
"vue-cli-plugin-vuetify": "2.4.1",
"vue-cli-plugin-webpack-bundle-analyzer": "4.0.0",
"vue-masonry-css": "1.0.3",
"vue-template-compiler": "2.6.13",
"vue-template-compiler": "2.6.14",
"vuetify-loader": "1.7.2"
}
}
2 changes: 1 addition & 1 deletion frontend/src/components/BrushableHistogram.vue
Expand Up @@ -228,7 +228,7 @@ export default class BrushableHistogram extends Vue {
const stats = await this.projectsContext.actions.getChannelStats({
acquisitionId: this.activeAcquisitionId,
channelName: this.channel.name,
});
}) as IChannelStats;
const histogram = this.calcHistogram(stats);
this.renderHistogram(histogram);
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/components/ImageViewer.vue
Expand Up @@ -114,25 +114,29 @@ export default class ImageViewer extends Vue {
@Watch("channelStackImage")
async onChannelStackImageChanged(value) {
if (value && this.activeAcquisitionId) {
const prevBackgroundImage = this.scatterplot.get("backgroundImage");
if (prevBackgroundImage) {
this.scatterplot.set({
backgroundImage: null,
});
prevBackgroundImage.destroy();
}
const regl = this.scatterplot.get("regl");
const img = new Image();
img.crossOrigin = "";
img.onload = () => {
const prevBackgroundImage = this.scatterplot.get("backgroundImage");
const regl = this.scatterplot.get("regl");
this.scatterplot.set({
backgroundImage: regl.texture(img),
aspectRatio: this.activeAcquisition!.max_x / this.activeAcquisition!.max_y,
});
if (prevBackgroundImage) {
prevBackgroundImage.destroy();
}
if (this.applyMask && this.cellsByAcquisition && this.cellsByAcquisition.has(this.activeAcquisitionId!)) {
this.points = this.cellsByAcquisition.get(this.activeAcquisitionId!)!;
const x = transformToWebGl(this.points, this.activeAcquisition!.max_x, this.activeAcquisition!.max_y);
this.scatterplot.draw(x);
} else {
this.scatterplot.draw([]);
this.scatterplot.clear();
}
// this.scatterplot.deselect({ preventEvent: true });
};
Expand Down Expand Up @@ -237,7 +241,8 @@ export default class ImageViewer extends Vue {
const canvas = this.$refs.canvasWebGl as Element;
this.scatterplot = createScatterplot({
syncEvents: true,
syncEvents: false,
backgroundImage: null,
canvas: canvas,
opacity: 0.5,
pointSize: 2,
Expand Down

0 comments on commit c7fd998

Please sign in to comment.