Skip to content

Commit

Permalink
Merge pull request #333 from alan-turing-institute/development
Browse files Browse the repository at this point in the history
MILESTONE: Merging `development` into `main` 🎉
  • Loading branch information
KristinaUlicna authored Dec 1, 2023
2 parents 710af32 + 0be61a3 commit ab4f13f
Show file tree
Hide file tree
Showing 53 changed files with 2,895 additions and 1,033 deletions.
Binary file added assets/triangles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
106 changes: 74 additions & 32 deletions examples/show_data.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,94 @@
import click
import napari

import pandas as pd
import numpy as np

from grace.base import GraphAttrs
from grace.io.image_dataset import mrc_reader
from pathlib import Path

from grace.base import GraphAttrs
from grace.io.image_dataset import FILETYPES

# Expects the image data & H5 node positions in the same folder.
# Use identical naming convention for files & specify whole path to mrc file:
# e.g. /Users/kulicna/Desktop/dataset/shape_squares/MRC_Synthetic_File_000.mrc

IMAGE_PATH = Path(
input(
"Enter absolute path to your file "
"(e.g. /Users/path/to/your/data/image.mrc, omit ''): "
)
# Define a click command to input the file name directly:
@click.command(name="Napari Annotator")
@click.option(
"--image_path",
type=click.Path(exists=True),
help="Path to the image to open in napari annotator",
)
NODES_PATH = Path(str(IMAGE_PATH).replace(".mrc", ".h5"))
@click.option(
"--show_features",
type=bool,
default=True,
help="Show feature representation of individual nodes",
)
def run_napari_annotator(
image_path: str | Path,
show_features: bool = False,
) -> None:
"""Function to open an image & annotate it in napari.
image_data = mrc_reader(IMAGE_PATH)
nodes_data = pd.read_hdf(NODES_PATH)
Parameters
----------
image_path : str | Path
Absolute filename of the image to be opened.
Expects the image data & H5 node positions in the same folder.
Use identical naming convention for these files to pair them up.
show_features : bool
Whether to display node features stored in the `h5` file.
Defaults to False.
points = np.asarray(nodes_data.loc[:, [GraphAttrs.NODE_Y, GraphAttrs.NODE_X]])
# features = {
# GraphAttrs.NODE_FEATURES:
# [np.squeeze(f.numpy()) for f in nodes_data.loc[:, "features"]]
# }
features = None
data_name = f"{IMAGE_PATH.stem}"
Notes
-----
- expected file organisation:
/path/to/your/image/MRC_Synthetic_File_000.mrc
...identical to...
/path/to/your/nodes/MRC_Synthetic_File_000.h5
"""
# Process the image data + load nodes:
suffix = str(image_path).split(".")[-1]
assert suffix in FILETYPES, f"Choose these filetypes: {FILETYPES.keys()}"

mn, mx = np.min(image_data), np.max(image_data)
image_reader = FILETYPES[suffix]
image_data = image_reader(Path(image_path))

viewer = napari.Viewer()
img_layer = viewer.add_image(
image_data, name=data_name, contrast_limits=(mn, mx)
)
pts_layer = viewer.add_points(
points, features=features, size=32, name=f"nodes_{data_name}"
)
nodes_path = image_path.replace(".mrc", ".h5")
nodes_data = pd.read_hdf(Path(nodes_path))

data_name = f"{Path(image_path).stem}"

# Start a napari window:
viewer = napari.Viewer()
mn, mx = np.min(image_data), np.max(image_data)
viewer.add_image(image_data, name=data_name, contrast_limits=(mn, mx))

# Locate the nodes as points:
points = np.asarray(
nodes_data.loc[:, [GraphAttrs.NODE_Y, GraphAttrs.NODE_X]]
)

# Process the node features:
if show_features is True:
features = {
"node_central_pixel_value": [
image_data[int(point[0]), int(point[1])] for point in points
]
}
else:
features = None

viewer.add_points(
points, features=features, size=32, name=f"nodes_{data_name}"
)

viewer.window.add_plugin_dock_widget(
plugin_name="grace", widget_name="GRACE"
)
napari.run()

_, widget = viewer.window.add_plugin_dock_widget(
plugin_name="grace", widget_name="GRACE"
)

if __name__ == "__main__":
# The napari event loop needs to be run under here to allow the window
# to be spawned from a Python script
napari.run()
run_napari_annotator()
77 changes: 74 additions & 3 deletions grace/base.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from __future__ import annotations

import dataclasses
import enum
import networkx as nx
import numpy as np
import numpy.typing as npt
import pandas as pd

from typing import Any
from dataclasses import dataclass
from scipy.spatial import Delaunay


Expand All @@ -19,12 +19,14 @@ class GraphAttrs(str, enum.Enum):
NODE_Y = "y"
NODE_GROUND_TRUTH = "node_ground_truth"
NODE_PREDICTION = "node_prediction"
NODE_FEATURES = "features"
NODE_IMG_EMBEDDING = "node_image_patch_latent_embedding"
NODE_ENV_EMBEDDING = "laplacian_matrix_multiplied_embedding"
NODE_CONFIDENCE = "confidence"
EDGE_SOURCE = "source"
EDGE_TARGET = "target"
EDGE_GROUND_TRUTH = "edge_ground_truth"
EDGE_PREDICTION = "edge_prediction"
EDGE_PROPERTIES = "edge_properties"


class Annotation(enum.IntEnum):
Expand All @@ -35,7 +37,7 @@ class Annotation(enum.IntEnum):
UNKNOWN = 2


@dataclasses.dataclass
@dataclass
class Prediction:
"""Prediction dataclass all normalised softmax class probabilities.
Expand Down Expand Up @@ -89,6 +91,75 @@ def prob_UNKNOWN(self) -> float:
return self.softmax_probs[Annotation.UNKNOWN]


@enum.unique
class EdgeProps(str, enum.Enum):
"""Ordered list of edge properties to be used for classifier training."""

EDGE_LENGTH = "edge_length_nrm"
EDGE_ORIENT = "edge_orient_rad"
EAST_NEIGHBOUR_LENGTH = "east_to_mid_length_nrm"
WEST_NEIGHBOUR_LENGTH = "west_to_mid_length_nrm"
EAST_NEIGHBOUR_ORIENT = "east_to_mid_orient_rad"
WEST_NEIGHBOUR_ORIENT = "west_to_mid_orient_rad"
EAST_TRIANGLE_AREA = "east_triangle_area_nrm"
WEST_TRIANGLE_AREA = "west_triangle_area_nrm"
EAST_DISTANCE = "east_to_mid_length_rel"
WEST_DISTANCE = "west_to_mid_length_rel"


@enum.unique
class PointCoords(str, enum.Enum):
"""Ordered list of relative point positions (x, y coords) in the graph."""

SOUTH_POS_X_REL = "south_pos_x_rel"
SOUTH_POS_Y_REL = "south_pos_y_rel"
NORTH_POS_X_REL = "north_pos_x_rel"
NORTH_POS_Y_REL = "north_pos_y_rel"
MID_POS_X_REL = "mid_pos_x_rel"
MID_POS_Y_REL = "mid_pos_y_rel"
EAST_POS_X_REL = "east_pos_x_rel"
EAST_POS_Y_REL = "east_pos_y_rel"
WEST_POS_X_REL = "west_pos_x_rel"
WEST_POS_Y_REL = "west_pos_y_rel"


@dataclass
class Properties:
"""Structure to organise the key: value pairs of EdgeProps properties."""

properties_dict: dict[str, float] = None

@property
def property_keys(self) -> list[str]:
return list(self.properties_dict.keys())

@property
def property_vals(self) -> list[float]:
return list(self.properties_dict.values())

@property
def property_training_data(
self, include_relative_coords: bool = False
) -> npt.NDArray:
if include_relative_coords is False:
return np.stack(
[self.properties_dict[prop] for prop in EdgeProps], axis=0
)
else:
keys = list(EdgeProps) + list(PointCoords)
return np.stack(
[self.properties_dict[prop] for prop in keys], axis=0
)

def from_keys_and_values(
self, keys: list[str], values: list[float]
) -> None:
assert len(keys) == len(values)
assert all(isinstance(k, str) for k in keys)
assert all(isinstance(v, (float, np.floating)) for v in values)
self.properties_dict = {k: v for k, v in zip(keys, values)}


def _map_annotation(annotation: int | Annotation) -> Annotation:
if isinstance(annotation, Annotation):
return Annotation
Expand Down
1 change: 1 addition & 0 deletions grace/evaluation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from grace import styling # noqa: F401
Loading

0 comments on commit ab4f13f

Please sign in to comment.