diff --git a/benchmarks/annotation_nquery.ipynb b/benchmarks/annotation_nquery.ipynb
new file mode 100644
index 000000000..ffd8949a7
--- /dev/null
+++ b/benchmarks/annotation_nquery.ipynb
@@ -0,0 +1,591 @@
+{
+ "cells": [
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "48e99d43-198f-4c4c-9425-95fec686c4b4",
+ "metadata": {},
+ "source": [
+ "# AnnotationStore Neighbourhood Querying Benchmark\n",
+ "\n",
+ "In this notebook the neighbourhood querying capability of the toolbox is benchmarked.\n",
+ "\n",
+ "Here we will be searching for annotations labelled \"A\" within a distance $k$ of annotations labeleld \"B\":\n",
+ "\n",
+ "\n",
+ "\n",
+ "This is done using the `AnnotationStore.nquery` function which is implemented in the abstract base class. This means that all concrete classes (e.g. `DictionaryStore` and `SQLiteStore`) get this functionality. However, it may be overriden in a subclass for improved performance or add implementation specific parameters.\n",
+ "\n",
+ "In this benchmark we asses how `nquery()` performs for the task of finding overlapping duplicate geometries. We simply generate a grid of $n \\times n$ aritificial cell boundary polygons (random ovals with noise added) labelled with class \"A\". We then generate another grid of the same side over the top labelled with class \"B\". We then query with `nquery()` to find all boundaries labelled \"A\" within a short distance of boundaries labelled \"B\".\n",
+ "\n",
+ "For a na\u00efve implementation this is an $\\mathcal{O}(n \\times n)$ search and is very slow! Some optimisations in the `SQLiteStore` should allow for this to be performed much more quickly.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c56c076e-c7e8-4682-9187-31415d3e4a3d",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "# Setup\n",
+ "\n",
+ "Here we will import the modules required, print the `nquery()` docstring, and define a function to generate cell boundaries.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "82e29bb9-0610-47cb-98ec-214fda0dc8fe",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Import packages needed\n",
+ "import sys\n",
+ "\n",
+ "sys.path.append(\"..\")\n",
+ "\n",
+ "import shutil\n",
+ "from time import perf_counter\n",
+ "from typing import Tuple, List\n",
+ "\n",
+ "import numpy as np\n",
+ "from shapely import affinity\n",
+ "from shapely.geometry import Polygon\n",
+ "\n",
+ "# NOTE: This notebook uses maptlotlib 2.3.7+ features\n",
+ "from matplotlib import pyplot as plt\n",
+ "import pandas as pd\n",
+ "\n",
+ "from tiatoolbox.annotation.storage import (\n",
+ " Annotation,\n",
+ " AnnotationStore,\n",
+ " DictionaryStore,\n",
+ " SQLiteStore,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5effd526-8d33-4c5c-b8d6-7d78dbe9c4d6",
+ "metadata": {},
+ "source": [
+ "Before we continue, let's print the docstring for `nquery()`:\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "a0581a43-13f3-4ca2-b2c4-e735a5f4eef0",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Help on function nquery in module tiatoolbox.annotation.storage:\n",
+ "\n",
+ "nquery(self, geometry: Union[shapely.geometry.point.Point, shapely.geometry.polygon.Polygon, shapely.geometry.linestring.LineString, NoneType] = None, where: Union[str, bytes, Callable[[Dict[str, Union[Dict, List, numbers.Number, str]]], bool], NoneType] = None, n_where: Union[str, bytes, Callable[[Dict[str, Union[Dict, List, numbers.Number, str]]], bool], NoneType] = None, distance: float = 5.0, geometry_predicate: str = 'intersects', mode: str = 'poly-poly') -> Dict[str, Dict[str, tiatoolbox.annotation.storage.Annotation]]\n",
+ " Query for annotations within a distance of another annotation.\n",
+ " \n",
+ " Args:\n",
+ " geometry (Geometry):\n",
+ " A geometry to use for the query. If None, the geometry of\n",
+ " the annotation is used. Defaults to None.\n",
+ " where (str or bytes or Callable):\n",
+ " A statement which should evaluate to a boolean value.\n",
+ " Only annotations for which this predicate is true will be\n",
+ " returned. Defaults to None (assume always true). This may\n",
+ " be a string, callable, or pickled function as bytes.\n",
+ " Callables are called to filter each result returned the\n",
+ " annotation store backend in python before being returned\n",
+ " to the user. A pickle object is, where possible, hooked\n",
+ " into the backend as a user defined function to filter\n",
+ " results during the backend query. Strings are expected to\n",
+ " be in a domain specific language and are converted to SQL\n",
+ " on a best-effort basis. For supported operators of the DSL\n",
+ " see :mod:`tiatoolbox.annotation.dsl`. E.g. a simple python\n",
+ " expression `props[\"class\"] == 42` will be converted to a\n",
+ " valid SQLite predicate when using `SQLiteStore` and\n",
+ " inserted into the SQL query. This should be faster than\n",
+ " filtering in python after or during the query. It is\n",
+ " important to note that untrusted user input should never\n",
+ " be accepted to this argument as arbitrary code can be\n",
+ " run via pickle or the parsing of the string statement.\n",
+ " n_where (str or bytes or Callable):\n",
+ " Predicate to filter the nearest annotations by. Defaults\n",
+ " to None (assume always true). See `where` for more\n",
+ " details.\n",
+ " distance (float):\n",
+ " The distance to search for annotations within. Defaults to\n",
+ " 5.0.\n",
+ " geometry_predicate (str):\n",
+ " The predicate to use when comparing geometries. Defaults\n",
+ " to \"intersects\". Other options include \"within\" and\n",
+ " \"contains\". Ignored if `mode` is \"boxpoint-boxpoint\" or\n",
+ " \"box-box\".\n",
+ " mode (tuple[str, str] or str):\n",
+ " The method to use for determining distance during the\n",
+ " query. Defaults to \"box-box\". This may significantly\n",
+ " change performance depending on the backend. Possible\n",
+ " options are:\n",
+ " - \"poly-poly\": Polygon boundary to polygon boundary.\n",
+ " - \"boxpoint-boxpoint\": Bounding box centre point to\n",
+ " bounding box centre point.\n",
+ " - \"box-box\": Bounding box to bounding box.\n",
+ " May be specified as a dash separated string or a tuple\n",
+ " of two strings. The first string is the mode for the\n",
+ " query geometry and the second string is the mode for\n",
+ " the nearest annotation geometry.\n",
+ " \n",
+ " Returns:\n",
+ " Dict[str, Dict[str, Annotation]]:\n",
+ " A dictionary mapping annotation keys to another\n",
+ " dictionary which represents an annotation key and all\n",
+ " annotations within `distance` of it.\n",
+ " \n",
+ " The `mode` argument is used to determine how to calculate the\n",
+ " distance between annotations. The default mode is \"box-box\".\n",
+ " \n",
+ " The \"box-box\" mode uses the bounding boxes of stored annotations\n",
+ " and the query geometry when determining if annotations are\n",
+ " within the neighbourhood.\n",
+ " \n",
+ " .. figure:: ../images/nquery-box-box.png\n",
+ " :width: 512\n",
+ " :alt: \"box-box\" mode\n",
+ " \n",
+ " The \"poly-poly\" performs full polygon-polygon intersection with\n",
+ " the polygon boundary of stored annotations and the query\n",
+ " geometry to determine if annotations are within the\n",
+ " neighbourhood.\n",
+ " \n",
+ " .. figure:: ../images/nquery-poly-poly.png\n",
+ " :width: 512\n",
+ " :alt: \"poly-poly\" mode\n",
+ " \n",
+ " \n",
+ " The \"boxpoint-boxpoint\" mode uses the centre point of the\n",
+ " bounding box of stored annotations and the query geometry when\n",
+ " determining if annotations are within the neighbourhood.\n",
+ " \n",
+ " .. figure:: ../images/nquery-boxpoint-boxpoint.png\n",
+ " :width: 512\n",
+ " :alt: \"boxpoint-boxpoint\" mode\n",
+ " \n",
+ " \n",
+ " Examples:\n",
+ " Example bounding boxy query with one neighbour within a\n",
+ " distance of 2.0.\n",
+ " \n",
+ " >>> from shapely.geometry import Point, Polyon\n",
+ " >>> from tiatoolbox.annotation.storage import Annotation, SQLiteStore\n",
+ " >>> store = SQLiteStore()\n",
+ " >>> annotation = Annotation(Point(0, 0), {\"class\": 42})\n",
+ " >>> store.add(annotation, \"foo\")\n",
+ " >>> neighbour = Annotation(Point(1, 1), {\"class\": 123})\n",
+ " >>> store.add(neighbour, \"bar\")\n",
+ " >>> store.nquery((-.5, -.5, .5, .5), distance=2.0)\n",
+ " {\n",
+ " \"foo\": {\n",
+ " Annotation(POINT (0 0), {'class': 42}): {\n",
+ " \"bar\": Annotation(POINT (1 1), {'class': 123}),\n",
+ " }\n",
+ " },\n",
+ " }\n",
+ " \n",
+ " Example bounding boxy query with no neighbours within a\n",
+ " distance of 1.0.\n",
+ " \n",
+ " >>> from shapely.geometry import Point\n",
+ " >>> from tiatoolbox.annotation.storage import Annotation, SQLiteStore\n",
+ " >>> store = SQLiteStore()\n",
+ " >>> annotation = Annotation(Point(0, 0), {\"class\": 42})\n",
+ " >>> store.add(annotation, \"foo\")\n",
+ " >>> store.nquery((-.5, -.5, .5, .5), distance=1.0)\n",
+ " {\"foo\": {Annotation(POINT (0 0), {'class': 42}): {}}}\n",
+ " \n",
+ " Example of querying for TILs - lympocytes within 3 units\n",
+ " of tumour cells.\n",
+ " \n",
+ " >>> from tiatoolbox.annotation.storage import SQLiteStore\n",
+ " >>> store = SQLiteStore(\"hovernet-pannuke-output.db\")\n",
+ " >>> tils = store.nquery(\n",
+ " ... where=\"props['class'] == 1\", # Tumour cells\n",
+ " ... n_where=\"props['class'] == 0\", # Lymphocytes\n",
+ " ... distance=32.0, # n_where within 32 units of where\n",
+ " ... mode=\"point-point\", # Use point to point distance\n",
+ " ... )\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "help(AnnotationStore.nquery)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4bcff28a-2b7a-43fa-b320-6a8036445f39",
+ "metadata": {},
+ "source": [
+ "Now we define a function used to generate fake cell boundaries used in the benchmark:\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "de5c5e1e-5c28-4f06-a2c1-62234290fa32",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def cell_polygon(\n",
+ " xy: Tuple[float, float],\n",
+ " n_points: int = 20,\n",
+ " radius: float = 10,\n",
+ " noise: float = 0.01,\n",
+ " eccentricity: Tuple[float, float] = (1, 3),\n",
+ " repeat_first: bool = True,\n",
+ " direction: str = \"CCW\",\n",
+ ") -> Polygon:\n",
+ " \"\"\"Generate a fake cell boundary polygon.\n",
+ "\n",
+ " Cell boundaries are generated an ellipsoids with randomised eccentricity,\n",
+ " added noise, and a random rotation.\n",
+ "\n",
+ " Args:\n",
+ " xy (tuple(int)):\n",
+ " The x,y centre point to generate the cell boundary around.\n",
+ " n_points (int):\n",
+ " Number of points in the boundary. Defaults to 20.\n",
+ " radius (float):\n",
+ " Radius of the points from the centre. Defaults to 10.\n",
+ " noise (float):\n",
+ " Noise to add to the point locations. Defaults to 1.\n",
+ " eccentricity (tuple(float)):\n",
+ " Range of values (low, high) to use for randomised\n",
+ " eccentricity. Defaults to (1, 3).\n",
+ " repeat_first (bool):\n",
+ " Enforce that the last point is equal to the first.\n",
+ " direction (str):\n",
+ " Ordering of the points. Defaults to \"CCW\". Valid options\n",
+ " are: counter-clockwise \"CCW\", and clockwise \"CW\".\n",
+ "\n",
+ " \"\"\"\n",
+ " if repeat_first:\n",
+ " n_points -= 1\n",
+ "\n",
+ " # Generate points about an ellipse with random eccentricity\n",
+ " x, y = xy\n",
+ " alpha = np.linspace(0, 2 * np.pi - (2 * np.pi / n_points), n_points)\n",
+ " rx = radius * (np.random.rand() + 0.5)\n",
+ " ry = np.random.uniform(*eccentricity) * radius - 0.5 * rx\n",
+ " x = rx * np.cos(alpha) + x + (np.random.rand(n_points) - 0.5) * noise\n",
+ " y = ry * np.sin(alpha) + y + (np.random.rand(n_points) - 0.5) * noise\n",
+ " boundary_coords = np.stack([x, y], axis=1).tolist()\n",
+ "\n",
+ " # Copy first coordinate to the end if required\n",
+ " if repeat_first:\n",
+ " boundary_coords = boundary_coords + [boundary_coords[0]]\n",
+ "\n",
+ " # Swap direction\n",
+ " if direction.strip().lower() == \"cw\":\n",
+ " boundary_coords = boundary_coords[::-1]\n",
+ "\n",
+ " polygon = Polygon(boundary_coords)\n",
+ "\n",
+ " # Add random rotation\n",
+ " angle = np.random.rand() * 360\n",
+ " return affinity.rotate(polygon, angle, origin=\"centroid\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9552d283-9d01-4167-a519-2a3d37c889b6",
+ "metadata": {},
+ "source": [
+ "Let's quickly generate and display an example cell boundary to check what they look like:\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "585d7984-ab88-4a46-94e4-5d597c5982bb",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": "",
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "cell_polygon((0, 0))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "68975dc2-b5ed-4127-985b-5305bbb00052",
+ "metadata": {},
+ "source": [
+ "# Define The Benchmark & A Function To Plot Results\n",
+ "\n",
+ "This benchmark can take a very long time to run for high $n$. The results are logged to an CSV as the benchmark runs in case the script is interrupted e.g. by a power cut or shutdown command.\n",
+ "\n",
+ "The plot function then reads in the CVS to plot the results.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "f6dc5a30-1014-4a06-820d-e8a43d3af35e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def main(): # noqa: CCR001\n",
+ " \"\"\"Run the benchmark.\"\"\"\n",
+ " spacing = 30\n",
+ " radius = 5\n",
+ " results = {}\n",
+ " classes = (SQLiteStore, DictionaryStore)\n",
+ " max_cls_name = max(len(cls.__name__) for cls in classes)\n",
+ " modes = (\"poly-poly\", \"box-box\", \"boxpoint-boxpoint\")\n",
+ " max_mode_name = max(len(mode) for mode in modes)\n",
+ "\n",
+ " with open(\"nquery-benchmark.csv\", \"w\") as fh:\n",
+ " header = \" , \".join([\"n\", \"time\", \"class\", \"mode\"])\n",
+ " print(header)\n",
+ " fh.write(header + \"\\n\")\n",
+ "\n",
+ " for grid_size in range(10, 60, 10):\n",
+ " for cls in classes:\n",
+ " grid = np.ndindex((grid_size, grid_size))\n",
+ " store: AnnotationStore = cls()\n",
+ "\n",
+ " # Add annotations\n",
+ " for x, y in grid:\n",
+ " cell_a = cell_polygon(\n",
+ " xy=(x * spacing + radius, y * spacing + radius),\n",
+ " radius=radius,\n",
+ " )\n",
+ " ann_a = Annotation(cell_a, {\"class\": \"A\"})\n",
+ " cell_b = cell_polygon(\n",
+ " xy=(x * spacing + radius, y * spacing + radius),\n",
+ " radius=radius,\n",
+ " )\n",
+ " ann_b = Annotation(cell_b, {\"class\": \"B\"})\n",
+ "\n",
+ " store[f\"A_{x}_{y}\"] = ann_a\n",
+ " store[f\"B_{x}_{y}\"] = ann_b\n",
+ "\n",
+ " # Validate annotations were added\n",
+ " if len(store) != grid_size**2 * 2:\n",
+ " raise ValueError(\"Not all annotations were added.\")\n",
+ "\n",
+ " for mode in modes:\n",
+ " # Query\n",
+ " t0 = perf_counter()\n",
+ " result = store.nquery(\n",
+ " where=\"props['class'] == 'A'\",\n",
+ " n_where=\"props['class'] == 'B'\",\n",
+ " distance=2,\n",
+ " mode=mode,\n",
+ " )\n",
+ " t1 = perf_counter()\n",
+ " dt = t1 - t0\n",
+ "\n",
+ " # Validate\n",
+ " if not isinstance(result, dict):\n",
+ " raise ValueError(\"Result is not a dictionary.\")\n",
+ " if len(result) != grid_size**2:\n",
+ " raise ValueError(\"Result does not contain all annotations.\")\n",
+ " for v in result.values():\n",
+ " if len(v) != 1:\n",
+ " raise ValueError(\n",
+ " \"Result does not contain the correct number of \"\n",
+ " \"annotations.\"\n",
+ " )\n",
+ "\n",
+ " # Store results\n",
+ " results[(grid_size, cls.__name__)] = dt\n",
+ " csv_line = \" , \".join(\n",
+ " [\n",
+ " f\"{grid_size:>3}\",\n",
+ " f\"{f'{dt:>3.2f}':>8}\",\n",
+ " f\"{cls.__name__: <{max_cls_name}}\",\n",
+ " f\"{mode: <{max_mode_name}}\",\n",
+ " ]\n",
+ " )\n",
+ " print(csv_line)\n",
+ " fh.write(csv_line + \"\\n\")\n",
+ " fh.flush()\n",
+ "\n",
+ "\n",
+ "def plot_csv() -> Tuple[plt.Figure, List[plt.Axes]]:\n",
+ " \"\"\"Plot the results from the benchmark.\"\"\"\n",
+ " # Use latex for rendering if available\n",
+ " plt.rc(\"text\", usetex=True if shutil.which(\"latex\") else False)\n",
+ " # Use serif font\n",
+ " plt.rc(\"font\", family=\"serif\")\n",
+ "\n",
+ " data = pd.read_csv(\n",
+ " \"nquery-benchmark.csv\", sep=r\"\\s*,\\s*\", header=0, engine=\"python\"\n",
+ " )\n",
+ " data = data.pivot(index=[\"n\", \"class\"], columns=\"mode\", values=\"time\")\n",
+ " mode_groups = data.groupby(\"class\")\n",
+ " # Plot each class (group) in a different colour and each mode\n",
+ " # (series) in a different line style.\n",
+ " fig, axs = plt.subplots(1, 2, figsize=(12, 6), layout=\"constrained\")\n",
+ " for n, (cls, group) in enumerate(mode_groups):\n",
+ " for m, (mode, series) in enumerate(group.items()):\n",
+ " for a, ax in enumerate(axs):\n",
+ " ax.plot(\n",
+ " range(len(series.index)),\n",
+ " series,\n",
+ " label=f\"{cls}\\n({mode})\" if a == 0 else None,\n",
+ " c=f\"C{n}\",\n",
+ " linestyle=[\"--\", \":\", \"-\"][m],\n",
+ " )\n",
+ " ax.grid(\"on\")\n",
+ " ax.set_xlabel(\"$n$\")\n",
+ " ax.set_ylabel(\"Time (s)\")\n",
+ " ax.set_xticks(\n",
+ " range(len(group.index)),\n",
+ " labels=group.index.get_level_values(\"n\"),\n",
+ " )\n",
+ " fig.legend(loc=\"outside right\")\n",
+ " axs[1].set_yscale(\"log\")\n",
+ " axs[0].set_title(\"Linear Scale\")\n",
+ " axs[1].set_title(\"Log Scale\")\n",
+ " plt.suptitle(\n",
+ " \"Neighbourhood Query Performance For Two Overlaid\"\n",
+ " \" $n \\\\times n$ Grids of Polygons\"\n",
+ " )\n",
+ " return fig, axs"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "15643597-7a12-485f-bc91-1a5da1bb6bc8",
+ "metadata": {},
+ "source": [
+ "# Run The Benchmark\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "39205512-9cba-4c97-b294-60cbe7cf3dc0",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "n , time , class , mode\n",
+ " 10 , 0.04 , SQLiteStore , poly-poly \n",
+ " 10 , 0.02 , SQLiteStore , box-box \n",
+ " 10 , 0.03 , SQLiteStore , boxpoint-boxpoint\n",
+ " 10 , 0.05 , DictionaryStore , poly-poly \n",
+ " 10 , 1.55 , DictionaryStore , box-box \n",
+ " 10 , 1.04 , DictionaryStore , boxpoint-boxpoint\n",
+ " 20 , 0.15 , SQLiteStore , poly-poly \n",
+ " 20 , 0.08 , SQLiteStore , box-box \n",
+ " 20 , 0.14 , SQLiteStore , boxpoint-boxpoint\n",
+ " 20 , 0.62 , DictionaryStore , poly-poly \n",
+ " 20 , 24.59 , DictionaryStore , box-box \n",
+ " 20 , 16.28 , DictionaryStore , boxpoint-boxpoint\n",
+ " 30 , 0.33 , SQLiteStore , poly-poly \n",
+ " 30 , 0.19 , SQLiteStore , box-box \n",
+ " 30 , 0.42 , SQLiteStore , boxpoint-boxpoint\n",
+ " 30 , 2.96 , DictionaryStore , poly-poly \n",
+ " 30 , 124.78 , DictionaryStore , box-box \n",
+ " 30 , 82.49 , DictionaryStore , boxpoint-boxpoint\n",
+ " 40 , 0.59 , SQLiteStore , poly-poly \n",
+ " 40 , 0.33 , SQLiteStore , box-box \n",
+ " 40 , 0.98 , SQLiteStore , boxpoint-boxpoint\n",
+ " 40 , 9.18 , DictionaryStore , poly-poly \n",
+ " 40 , 397.54 , DictionaryStore , box-box \n",
+ " 40 , 263.33 , DictionaryStore , boxpoint-boxpoint\n",
+ " 50 , 0.93 , SQLiteStore , poly-poly \n",
+ " 50 , 0.54 , SQLiteStore , box-box \n",
+ " 50 , 2.04 , SQLiteStore , boxpoint-boxpoint\n",
+ " 50 , 22.64 , DictionaryStore , poly-poly \n",
+ " 50 , 990.49 , DictionaryStore , box-box \n",
+ " 50 , 660.43 , DictionaryStore , boxpoint-boxpoint\n"
+ ]
+ }
+ ],
+ "source": [
+ "main()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "13ed8581-fe13-471f-a590-b70f9a88c2b4",
+ "metadata": {},
+ "source": [
+ "# Plot Results\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "38f9e864-79b6-46a8-b6e7-ed25f68d95e8",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_csv()\n",
+ "plt.show()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/benchmarks/nquery-concept.svg b/benchmarks/nquery-concept.svg
new file mode 100644
index 000000000..f2a8ead2c
--- /dev/null
+++ b/benchmarks/nquery-concept.svg
@@ -0,0 +1,59 @@
+
+
+
diff --git a/docs/images/nquery-box-box.png b/docs/images/nquery-box-box.png
new file mode 100644
index 000000000..36270a8eb
Binary files /dev/null and b/docs/images/nquery-box-box.png differ
diff --git a/docs/images/nquery-boxpoint-boxpoint.png b/docs/images/nquery-boxpoint-boxpoint.png
new file mode 100644
index 000000000..2dc553af4
Binary files /dev/null and b/docs/images/nquery-boxpoint-boxpoint.png differ
diff --git a/docs/images/nquery-poly-poly.png b/docs/images/nquery-poly-poly.png
new file mode 100644
index 000000000..216b3e014
Binary files /dev/null and b/docs/images/nquery-poly-poly.png differ
diff --git a/tests/test_annotation_stores.py b/tests/test_annotation_stores.py
index c03bac0be..ccd6b565a 100644
--- a/tests/test_annotation_stores.py
+++ b/tests/test_annotation_stores.py
@@ -130,6 +130,15 @@ def annotations_center_of_mass(annotations):
return MultiPoint(centroids).centroid
+def test_annotation_repr():
+ """Test the repr of an annotation."""
+ annotation = Annotation(Polygon([(0, 0), (1, 1), (2, 0)]))
+ assert isinstance(repr(annotation), str)
+ assert repr(annotation).startswith("Annotation(")
+ assert "POLYGON" in repr(annotation)
+ assert repr(annotation).endswith(")")
+
+
# Fixtures
@@ -1642,6 +1651,480 @@ def test_connection_to_path_io(store_cls, tmp_path):
store_cls._connection_to_path(fh)
assert path == Path(fh.name)
+ @staticmethod
+ def test_nquery_boxpoint_boxpoint(store_cls):
+ """Test simple querying within a neighbourhood.
+
+ Test that a neighbourhood query returns the correct results
+ for a simple data store with two points.
+
+ .. code-block:: text
+
+ ^
+ 3|--****-----C
+ |* * |
+ 2| * |
+ | B *|
+ 1| A *
+ | *|
+ 0+---------*-->
+ 0 1 2 3
+
+ Query for all points within a distance of 2 from A. Should
+ return a dictionary with a single key, "A", and a value of
+ {"B": B}.
+
+ """
+ store: AnnotationStore = store_cls()
+ ann_a = Annotation(
+ Point(1, 1),
+ {"class": "A"},
+ )
+ store["A"] = ann_a
+ ann_b = Annotation(
+ Point(1.4, 1.4),
+ {"class": "B"},
+ )
+ store["B"] = ann_b
+ # C is inside the bounding box of the radius around A but is not
+ # returned because it is not inside of the radius.
+ ann_c = Annotation(
+ Point(2.9, 2.9),
+ {"class": "C"},
+ )
+ store["C"] = ann_c
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] != 'A'",
+ distance=2,
+ mode="boxpoint-boxpoint",
+ )
+ assert isinstance(result, dict)
+ assert len(result) == 1
+ assert "A" in result
+ assert result["A"] == {"B": ann_b}
+
+ @staticmethod
+ def test_nquery_boxpoint_boxpoint_no_results(store_cls):
+ """Test querying within a neighbourhood with no results.
+
+ Test that a neighbourhood query returns an empty dictionary
+ when there are no results.
+
+ .. code-block:: text
+
+ 3^
+ 2|
+ 1|
+ 0+----->
+ 0 1 2 3
+
+ Query for all points within a distance of 2 from A. Should
+ return an empty dictionary.
+
+ """
+ store: AnnotationStore = store_cls()
+ ann_a = Annotation(
+ Point(1, 1),
+ {"class": "A"},
+ )
+ store["A"] = ann_a
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=2,
+ mode="boxpoint-boxpoint",
+ )
+ assert isinstance(result, dict)
+ assert len(result) == 0
+
+ @staticmethod
+ def test_nquery_boxpoint_boxpoint_multiple(store_cls):
+ """Test querying within a neighbourhood with multiple results.
+
+ Test that a neighbourhood query returns the correct results
+ for a simple data store with four points.
+
+ .. code-block:: text
+
+ 3^
+ 2| B
+ 1| A C D <-- D is outside the neighbourhood
+ 0+------>
+ 0 1 2 3
+
+ Query for all points within a distance of 2 from A. Should
+ return a dictionary with a single key, "A", and a value of
+ {"B": B, "C": C}.
+
+ """
+ store: AnnotationStore = store_cls()
+ ann_a = Annotation(
+ Point(1, 1),
+ {"class": "A"},
+ )
+ store["A"] = ann_a
+
+ ann_b = Annotation(
+ Point(2, 2),
+ {"class": "B"},
+ )
+ store["B"] = ann_b
+
+ ann_c = Annotation(
+ Point(2, 1),
+ {"class": "C"},
+ )
+ store["C"] = ann_c
+
+ ann_d = Annotation(
+ Point(3, 1),
+ {"class": "D"},
+ )
+ store["D"] = ann_d
+
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="(props['class'] == 'B') | (props['class'] == 'C')",
+ distance=2,
+ mode="boxpoint-boxpoint",
+ )
+ assert isinstance(result, dict)
+ assert len(result) == 1
+ assert "A" in result
+ assert result["A"] == {"B": ann_b, "C": ann_c}
+
+ @staticmethod
+ def test_nquery_poly_poly(store_cls):
+ """Test querying within a neighbourhood with multiple results.
+
+ Test that a neighbourhood query returns the correct results
+ for a simple data store with two polygons.
+
+ .. code-block:: text
+
+ 3^
+ 2| B
+ 1| A
+ 0+------>
+ 0 1 2 3
+
+ """
+ store: AnnotationStore = store_cls()
+
+ ann_a = Annotation( # Triangle
+ Polygon([(0, 0), (0, 1), (1, 0)]),
+ {"class": "A"},
+ )
+ store["A"] = ann_a
+
+ ann_b = Annotation( # Square
+ Polygon.from_bounds(1, 1, 2, 2),
+ {"class": "B"},
+ )
+ store["B"] = ann_b
+
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=2,
+ mode="poly-poly",
+ )
+ assert isinstance(result, dict)
+ assert len(result) == 1
+
+ @staticmethod
+ def test_nquery_poly_poly_vs_boxpoint_boxpoint(store_cls):
+ """Test querying within a neighbourhood with two polygons.
+
+ Test that a full polygon neighbourhood query returns results
+ where a centroid query would return no results.
+
+ .. code-block:: text
+
+ ^
+ 3|
+ | <----2---->
+ 2| +-----+ +-----+
+ | | + |<-1->| + |
+ 1| +-----+ +-----+
+ |
+ 0+------------------------>
+ 0 1 2 3 4
+
+ """
+ store: AnnotationStore = store_cls()
+
+ ann_a = Annotation(
+ Polygon.from_bounds(1, 1, 2, 2),
+ {"class": "A"},
+ )
+ store["A"] = ann_a
+
+ ann_b = Annotation(
+ Polygon.from_bounds(3, 1, 4, 2),
+ {"class": "B"},
+ )
+ store["B"] = ann_b
+
+ distance = 1.25
+
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=distance,
+ mode="boxpoint-boxpoint",
+ )
+ assert isinstance(result, dict)
+ assert len(result) == 0
+
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=distance,
+ mode=("poly", "poly"),
+ )
+ assert isinstance(result, dict)
+ assert len(result) == 1
+
+ @staticmethod
+ def test_nquery_polygon_boundary_alt(store_cls):
+ """Test querying within a neighbourhood with two polygons.
+
+ This test is similar to test_nquery_polygon_boundary, but
+ the centroids are closer than the boundaries.
+
+ .. code-block:: text
+
+ ^
+ 5| +-----------------+
+ | +---------------+ |
+ 4| <-----2----->| | centroid-boundary = 2
+ | <--1--> | | centroid-centroid = 1
+ 3| +-----+ | |
+ | | + | + ^ | | centroid-boundary = 2
+ 2| +-----+ | | |
+ | ^ |2 | |
+ 1| v1.5 v | | boundary-boundary = 1.5
+ | +---------------+ |
+ 0+-----+-----------------+-->
+ 0 1 2 3 4
+ """
+ store: AnnotationStore = store_cls()
+
+ # Annotation A: A 1x1 box
+ ann_a = Annotation(
+ Polygon.from_bounds(1, 2, 2, 3),
+ {"class": "A"},
+ )
+ store["A"] = ann_a
+
+ # C shaped polygon around annotation A
+ ann_b = Annotation(
+ Polygon(
+ [
+ (1, 0),
+ (4, 0),
+ (4, 5),
+ (1, 5),
+ (1, 4.5),
+ (3.5, 4.5),
+ (3.5, 0.5),
+ (1, 0.5),
+ ]
+ ),
+ {"class": "B"},
+ )
+ store["B"] = ann_b
+
+ distance = 1.75
+
+ centroid = Polygon.from_bounds(*ann_b.geometry.bounds).centroid
+
+ print(centroid)
+ print(ann_a.geometry.centroid)
+ print(
+ centroid.buffer(distance)
+ .intersection(ann_a.geometry.centroid.buffer(distance))
+ .area
+ )
+
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=distance,
+ mode="boxpoint-boxpoint",
+ )
+ assert isinstance(result, dict)
+ assert len(result) == 1
+
+ @staticmethod
+ def test_nquery_overlapping_grid_box_box(store_cls):
+ """Find duplicate (overlapping) cell boundaries via bounding boxes.
+
+ This generates an :math:`n \\times n` (where :math:`n=10`) grid
+ of overlapping fake cell boundary polygons, where each polygon
+ has radius of 5 and the grid has a spacing of 30.
+
+ The grid is then queried with a "box-box" neighbourhood query
+ (intersection of bounding boxes) and a `distance` paramete of 0
+ (no expansion of bounding boxes).
+
+ """
+ store: AnnotationStore = store_cls()
+
+ grid_size = 10
+ spacing = 30
+ radius = 5
+ grid = np.ndindex((grid_size, grid_size))
+
+ for x, y in grid:
+ cell_a = cell_polygon(
+ (x * spacing + radius, y * spacing + radius), radius=radius
+ )
+ ann_a = Annotation(cell_a, {"class": "A"})
+ cell_b = cell_polygon(
+ (x * spacing + radius, y * spacing + radius), radius=radius
+ )
+ ann_b = Annotation(cell_b, {"class": "B"})
+
+ store[f"A_{x}_{y}"] = ann_a
+ store[f"B_{x}_{y}"] = ann_b
+
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=0,
+ mode="box-box",
+ )
+ assert isinstance(result, dict)
+ assert len(result) == grid_size**2
+ for v in result.values():
+ assert len(v) == 1
+
+ @staticmethod
+ def test_nquery_overlapping_grid_boxpoint_boxpoint(store_cls):
+ """Find duplicate (overlapping) cell boundaries via bbox centroid distance.
+
+ This generates an :math:`n \\times n` (where :math:`n=10`) grid
+ of overlapping fake cell boundary polygons, where each polygon
+ has radius of 5 and the grid has a spacing of 30.
+
+ The grid is then queried with a "boxpoint-boxpoint"
+ neighbourhood query and a `distance` of 2 (use a buffer of 2
+ around the point).
+
+ """
+ store: AnnotationStore = store_cls()
+
+ grid_size = 10
+ spacing = 10
+ radius = 5
+ grid = np.ndindex((grid_size, grid_size))
+
+ for x, y in grid:
+ cell_a = cell_polygon(
+ (x * spacing + radius, y * spacing + radius), radius=radius
+ )
+ ann_a = Annotation(cell_a, {"class": "A"})
+ cell_b = cell_polygon(
+ (x * spacing + radius, y * spacing + radius), radius=radius
+ )
+ ann_b = Annotation(cell_b, {"class": "B"})
+
+ store[f"A_{x}_{y}"] = ann_a
+ store[f"B_{x}_{y}"] = ann_b
+
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=2,
+ mode="boxpoint-boxpoint",
+ )
+ assert isinstance(result, dict)
+ assert len(result) == grid_size**2
+ for v in result.values():
+ assert len(v) == 1
+
+ @staticmethod
+ def test_nquery_overlapping_grid_poly_poly(store_cls):
+ """Find duplicate (overlapping) cell boundaries via polygon intersection.
+
+ This generates an :math:`n \\times n` (where :math:`n=10`) grid
+ of overlapping fake cell boundary polygons, where each polygon
+ has radius of 5 and the grid has a spacing of 30.
+
+ The grid is then queried with a "poly-poly" neighbourhood query
+ (intersection of polygons) and a `distance` parameter of 2.
+
+ """
+ store: AnnotationStore = store_cls()
+
+ grid_size = 10
+ spacing = 30
+ radius = 5
+ grid = np.ndindex((grid_size, grid_size))
+
+ for x, y in grid:
+ cell_a = cell_polygon(
+ (x * spacing + radius, y * spacing + radius), radius=radius
+ )
+ ann_a = Annotation(cell_a, {"class": "A"})
+ cell_b = cell_polygon(
+ (x * spacing + radius, y * spacing + radius), radius=radius
+ )
+ ann_b = Annotation(cell_b, {"class": "B"})
+
+ store[f"A_{x}_{y}"] = ann_a
+ store[f"B_{x}_{y}"] = ann_b
+
+ result = store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=2,
+ mode="poly-poly",
+ )
+ assert isinstance(result, dict)
+ assert len(result) == grid_size**2
+ for v in result.values():
+ assert len(v) == 1
+
+ @staticmethod
+ def test_invalid_mode_type(store_cls):
+ store: AnnotationStore = store_cls()
+
+ with pytest.raises(TypeError, match="string or tuple of strings"):
+ store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=2,
+ mode=123,
+ )
+
+ @staticmethod
+ def test_invalid_mode_format(store_cls):
+ store: AnnotationStore = store_cls()
+
+ with pytest.raises(ValueError, match="must be one of"):
+ store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=2,
+ mode="invalid-invalid-invalid",
+ )
+
+ @staticmethod
+ def test_invalid_mode(store_cls):
+ store: AnnotationStore = store_cls()
+
+ with pytest.raises(ValueError, match="must be one of"):
+ store.nquery(
+ where="props['class'] == 'A'",
+ n_where="props['class'] == 'B'",
+ distance=2,
+ mode="invalid",
+ )
+
@staticmethod
def test_bquery_only_where(store_cls):
"""Test that bquery when only a where predicate is given.
diff --git a/tests/test_dsl.py b/tests/test_dsl.py
index d2562e184..ff3617195 100644
--- a/tests/test_dsl.py
+++ b/tests/test_dsl.py
@@ -62,7 +62,7 @@ def test_json_contains():
def sqlite_eval(query: Union[str, Number]):
- """Evaluate an SQL predicate on dummpy data and return the result.
+ """Evaluate an SQL predicate on dummy data and return the result.
Args:
query (Union[str, Number]): SQL predicate to evaluate.
@@ -91,6 +91,21 @@ def sqlite_eval(query: Union[str, Number]):
return result
+class TestSQLite: # noqa: PIE798
+ """Test converting from our DSL to an SQLite backend."""
+
+ @staticmethod
+ def test_prop_or_prop():
+ """Test OR operator between two prop accesses."""
+ query = eval( # skipcq: PYL-W0123
+ "(props['int'] == 2) | (props['int'] == 3)", SQL_GLOBALS, {}
+ )
+ assert str(query) == (
+ '((json_extract(properties, "$.int") == 2) OR '
+ '(json_extract(properties, "$.int") == 3))'
+ )
+
+
class TestPredicate:
"""Test predicate statments with various backends."""
diff --git a/tiatoolbox/annotation/storage.py b/tiatoolbox/annotation/storage.py
index b32ce3579..0de01a168 100644
--- a/tiatoolbox/annotation/storage.py
+++ b/tiatoolbox/annotation/storage.py
@@ -147,6 +147,9 @@ def to_geojson(self) -> str:
"""
return json.dumps(self.to_feature())
+ def __repr__(self) -> str:
+ return f"Annotation({self.geometry}, {self.properties})"
+
class AnnotationStore(ABC, MutableMapping):
"""Annotation store abstract base class."""
@@ -295,7 +298,11 @@ def _geometry_predicate(name: str, a: Geometry, b: Geometry) -> Callable:
"overlaps",
"touches",
"within",
- "bbox_intersects", # Special non-shapely case, bounding-boxes intersect
+ # Special non-shapely case, bounding-boxes intersect.
+ "bbox_intersects",
+ # Special non-shapely case, query centroid within k of
+ # annotation bounds center.
+ "centers_within_k",
]
@classmethod # noqa: A003
@@ -619,6 +626,7 @@ def query(
geometry: Optional[QueryGeometry] = None,
where: Optional[Predicate] = None,
geometry_predicate: str = "intersects",
+ distance: float = 0,
) -> Dict[str, Annotation]:
"""Query the store for annotations.
@@ -661,6 +669,9 @@ def query(
"intersects". For more information see the `shapely
documentation on binary predicates `_.
+ distance (float):
+ Distance used when performing a distance based query.
+ E.g. "centers_within_k" geometry predicate.
Returns:
list:
@@ -677,6 +688,27 @@ def query(
query_geometry = geometry
if isinstance(query_geometry, Iterable):
query_geometry = Polygon.from_bounds(*query_geometry)
+ if geometry_predicate == "centers_within_k":
+ query_point = Polygon.from_bounds(*query_geometry.bounds).centroid
+
+ def bbox_intersects(
+ annotation_geometry: Geometry, query_geometry: Geometry
+ ) -> bool:
+ """True if bounding box of the annotation intersects the query geometry."""
+ return Polygon.from_bounds(*query_geometry.bounds).intersects(
+ Polygon.from_bounds(*annotation_geometry.bounds)
+ )
+
+ def centers_within_k(
+ annotation_geometry: Geometry, query_point: Point, distance: float
+ ) -> bool:
+ """True if centre of annotation within k of query geometry center.
+
+ Here the "center" is the centroid of the bounds.
+
+ """
+ ann_centre = Polygon.from_bounds(*annotation_geometry.bounds).centroid
+ return query_point.dwithin(ann_centre, distance)
def filter_function(annotation: Annotation) -> bool:
"""Filter function for querying annotations.
@@ -693,8 +725,26 @@ def filter_function(annotation: Annotation) -> bool:
"""
return ( # Geometry is None or the geometry predicate matches
query_geometry is None
- or self._geometry_predicate(
- geometry_predicate, query_geometry, annotation.geometry
+ or any(
+ [
+ (
+ geometry_predicate == "bbox_intersects"
+ and bbox_intersects(annotation.geometry, query_geometry)
+ ),
+ (
+ geometry_predicate == "centers_within_k"
+ and centers_within_k(
+ annotation.geometry, query_point, distance
+ )
+ ),
+ (
+ geometry_predicate
+ not in ("bbox_intersects", "centers_within_k")
+ and self._geometry_predicate(
+ geometry_predicate, query_geometry, annotation.geometry
+ )
+ ),
+ ]
)
) and self._eval_where(where, annotation.properties)
@@ -1006,6 +1056,196 @@ def select_values(
select, unique, squeeze, items, select_values
)
+ def nquery(
+ self,
+ geometry: Optional[Geometry] = None,
+ where: Optional[Predicate] = None,
+ n_where: Optional[Predicate] = None,
+ distance: float = 5.0,
+ geometry_predicate: str = "intersects",
+ mode: str = "poly-poly",
+ ) -> Dict[str, Dict[str, Annotation]]:
+ """Query for annotations within a distance of another annotation.
+
+ Args:
+ geometry (Geometry):
+ A geometry to use to query for the initial set of
+ annotations to perform a neighbourhood search around. If
+ None, all annotations in the store are considered.
+ Defaults to None.
+ where (str or bytes or Callable):
+ A statement which should evaluate to a boolean value.
+ Only annotations for which this predicate is true will be
+ returned. Defaults to None (assume always true). This may
+ be a string, callable, or pickled function as bytes.
+ Callables are called to filter each result returned the
+ annotation store backend in python before being returned
+ to the user. A pickle object is, where possible, hooked
+ into the backend as a user defined function to filter
+ results during the backend query. Strings are expected to
+ be in a domain specific language and are converted to SQL
+ on a best-effort basis. For supported operators of the DSL
+ see :mod:`tiatoolbox.annotation.dsl`. E.g. a simple python
+ expression `props["class"] == 42` will be converted to a
+ valid SQLite predicate when using `SQLiteStore` and
+ inserted into the SQL query. This should be faster than
+ filtering in python after or during the query. It is
+ important to note that untrusted user input should never
+ be accepted to this argument as arbitrary code can be
+ run via pickle or the parsing of the string statement.
+ n_where (str or bytes or Callable):
+ Predicate to filter the nearest annotations by. Defaults
+ to None (assume always true). See `where` for more
+ details.
+ distance (float):
+ The distance to search for annotations within. Defaults to
+ 5.0.
+ geometry_predicate (str):
+ The predicate to use when comparing geometries. Defaults
+ to "intersects". Other options include "within" and
+ "contains". Ignored if `mode` is "boxpoint-boxpoint" or
+ "box-box".
+ mode (tuple[str, str] or str):
+ The method to use for determining distance during the
+ query. Defaults to "box-box". This may significantly
+ change performance depending on the backend. Possible
+ options are:
+ - "poly-poly": Polygon boundary to polygon boundary.
+ - "boxpoint-boxpoint": Bounding box centre point to
+ bounding box centre point.
+ - "box-box": Bounding box to bounding box.
+ May be specified as a dash separated string or a tuple
+ of two strings. The first string is the mode for the
+ query geometry and the second string is the mode for
+ the nearest annotation geometry.
+
+ Returns:
+ Dict[str, Dict[str, Annotation]]:
+ A dictionary mapping annotation keys to another
+ dictionary which represents an annotation key and all
+ annotations within `distance` of it.
+
+ The `mode` argument is used to determine how to calculate the
+ distance between annotations. The default mode is "box-box".
+
+ The "box-box" mode uses the bounding boxes of stored annotations
+ and the query geometry when determining if annotations are
+ within the neighbourhood.
+
+ .. figure:: ../images/nquery-box-box.png
+ :width: 512
+ :alt: "box-box" mode
+
+ The "poly-poly" performs full polygon-polygon intersection with
+ the polygon boundary of stored annotations and the query
+ geometry to determine if annotations are within the
+ neighbourhood.
+
+ .. figure:: ../images/nquery-poly-poly.png
+ :width: 512
+ :alt: "poly-poly" mode
+
+
+ The "boxpoint-boxpoint" mode uses the centre point of the
+ bounding box of stored annotations and the query geometry when
+ determining if annotations are within the neighbourhood.
+
+ .. figure:: ../images/nquery-boxpoint-boxpoint.png
+ :width: 512
+ :alt: "boxpoint-boxpoint" mode
+
+
+ Examples:
+ Example bounding box query with one neighbour within a
+ distance of 2.0.
+
+ >>> from shapely.geometry import Point, Polyon
+ >>> from tiatoolbox.annotation.storage import Annotation, SQLiteStore
+ >>> store = SQLiteStore()
+ >>> annotation = Annotation(Point(0, 0), {"class": 42})
+ >>> store.append(annotation, "foo")
+ >>> neighbour = Annotation(Point(1, 1), {"class": 123})
+ >>> store.add(neighbour, "bar")
+ >>> store.nquery((-.5, -.5, .5, .5), distance=2.0)
+ {
+ "foo": {
+ Annotation(POINT (0 0), {'class': 42}): {
+ "bar": Annotation(POINT (1 1), {'class': 123}),
+ }
+ },
+ }
+
+ Example bounding box query with no neighbours within a
+ distance of 1.0.
+
+ >>> from shapely.geometry import Point
+ >>> from tiatoolbox.annotation.storage import Annotation, SQLiteStore
+ >>> store = SQLiteStore()
+ >>> annotation = Annotation(Point(0, 0), {"class": 42})
+ >>> store.add(annotation, "foo")
+ >>> store.nquery((-.5, -.5, .5, .5), distance=1.0)
+ {"foo": {Annotation(POINT (0 0), {'class': 42}): {}}}
+
+ Example of querying for TILs - lympocytes within 3 units
+ of tumour cells.
+
+ >>> from tiatoolbox.annotation.storage import SQLiteStore
+ >>> store = SQLiteStore("hovernet-pannuke-output.db")
+ >>> tils = store.nquery(
+ ... where="props['class'] == 1", # Tumour cells
+ ... n_where="props['class'] == 0", # Lymphocytes
+ ... distance=32.0, # n_where within 32 units of where
+ ... mode="point-point", # Use point to point distance
+ ... )
+
+ """
+ # This is a naive generic implementation which can be overridden
+ # by back ends which can do this more efficiently.
+ if not isinstance(mode, (str, tuple)):
+ raise TypeError("mode must be a string or tuple of strings")
+ if isinstance(mode, str):
+ mode = tuple(mode.split("-"))
+ if mode not in (("box", "box"), ("boxpoint", "boxpoint"), ("poly", "poly")):
+ raise ValueError(
+ "mode must be one of 'box-box', 'boxpoint-boxpoint', or 'poly-poly'"
+ )
+ from_mode, _ = mode
+
+ # Initial selection of annotations to query around
+ selection = self.query(
+ geometry=geometry,
+ where=where,
+ )
+
+ # Query for others within the distance of initial selection
+ result = {}
+ for key, ann in selection.items():
+ geometry = ann.geometry
+ if from_mode == "box":
+ geometry_predicate = "bbox_intersects"
+ min_x, min_y, max_x, max_y = ann.geometry.bounds
+ geometry = Polygon.from_bounds(
+ min_x - distance,
+ min_y - distance,
+ max_x + distance,
+ max_y + distance,
+ )
+ elif from_mode == "boxpoint":
+ geometry_predicate = "centers_within_k"
+ elif from_mode == "poly": # pragma: no branch
+ geometry = ann.geometry
+ geometry = geometry.buffer(distance)
+ subquery_result = self.query(
+ geometry=geometry,
+ where=n_where,
+ geometry_predicate=geometry_predicate,
+ distance=distance,
+ )
+ if subquery_result:
+ result[key] = subquery_result
+
+ return result
+
@staticmethod
def _handle_pquery_results(
select: Select,
@@ -1549,7 +1789,9 @@ def __init__(
self.compression_level = self.metadata["compression_level"]
# Register predicate functions as custom SQLite functions
- def wkb_predicate(name: str, wkb_a: bytes, b: bytes, cx: int, cy: int) -> bool:
+ def wkb_predicate(
+ name: str, wkb_a: bytes, b: bytes, cx: float, cy: float
+ ) -> bool:
"""Wrapper function to allow WKB as inputs to binary predicates."""
a = wkb.loads(wkb_a)
b = self._unpack_geometry(b, cx, cy)
@@ -1561,7 +1803,7 @@ def pickle_expression(pickle_bytes: bytes, properties: str) -> bool:
properties = json.loads(properties)
return fn(properties)
- def get_area(wkb_bytes: bytes, cx: int, cy: int) -> float:
+ def get_area(wkb_bytes: bytes, cx: float, cy: float) -> float:
"""Function to get the area of a geometry."""
return self._unpack_geometry(
wkb_bytes,
@@ -1613,7 +1855,7 @@ def register_custom_function(
# Create tables for geometry and RTree index
self.con.execute(
"""
- CREATE VIRTUAL TABLE rtree USING rtree_i32(
+ CREATE VIRTUAL TABLE rtree USING rtree(
id, -- Integer primary key
min_x, max_x, -- 1st dimension min, max
min_y, max_y -- 2nd dimension min, max
@@ -1626,11 +1868,11 @@ def register_custom_function(
id INTEGER PRIMARY KEY, -- Integer primary key
key TEXT UNIQUE, -- Unique identifier (UUID)
objtype TEXT, -- Object type
- cx INTEGER NOT NULL, -- X of centroid/representative point
- cy INTEGER NOT NULL, -- Y of centroid/representative point
+ cx FLOAT NOT NULL, -- X of centroid/representative point
+ cy FLOAT NOT NULL, -- Y of centroid/representative point
geometry BLOB, -- Detailed geometry
properties TEXT, -- JSON properties
- area INTEGER NOT NULL -- Area (for ordering)
+ area FLOAT NOT NULL -- Area (for ordering)
)
"""
@@ -1663,7 +1905,9 @@ def serialise_geometry( # skipcq: PYL-W0221
return zlib.compress(data, level=self.compression_level)
raise ValueError("Unsupported compression method.")
- def _unpack_geometry(self, data: Union[str, bytes], cx: int, cy: int) -> Geometry:
+ def _unpack_geometry(
+ self, data: Union[str, bytes], cx: float, cy: float
+ ) -> Geometry:
"""Return the geometry using WKB data and rtree bounds index.
For space optimisation, points are stored as centroids and all
@@ -1676,7 +1920,7 @@ def _unpack_geometry(self, data: Union[str, bytes], cx: int, cy: int) -> Geometr
The WKB/WKT data to be unpacked.
cx(int):
The X coordinate of the centroid/representative point.
- cy(int):
+ cy(float):
The Y coordinate of the centroid/representative point.
Returns:
@@ -1766,15 +2010,15 @@ def _make_token(self, annotation: Annotation, key: Optional[str]) -> Dict:
return {
"key": key,
"geometry": serialised_geometry,
- "cx": int(geometry.centroid.x),
- "cy": int(geometry.centroid.y),
+ "cx": geometry.centroid.x,
+ "cy": geometry.centroid.y,
"min_x": geometry.bounds[0],
"min_y": geometry.bounds[1],
"max_x": geometry.bounds[2],
"max_y": geometry.bounds[3],
"geom_type": geometry.geom_type,
"properties": json.dumps(annotation.properties, separators=(",", ":")),
- "area": int(geometry.area),
+ "area": geometry.area,
}
def append_many(
@@ -1836,8 +2080,13 @@ def _append(self, key: str, annotation: Annotation, cur: sqlite3.Cursor) -> None
@staticmethod
def _initialize_query_string_parameters(
- query_geometry, query_parameters, geometry_predicate, columns, where
- ):
+ query_geometry: Optional[Geometry],
+ query_parameters: Dict[str, Any],
+ geometry_predicate: Optional[str],
+ columns: str,
+ where: Union[bytes, str],
+ distance: float = 0,
+ ) -> Tuple[str, Dict[str, Any]]:
"""Initialises the query string and parameters."""
query_string = (
"SELECT " # skipcq: BAN-B608
@@ -1851,13 +2100,25 @@ def _initialize_query_string_parameters(
# There is query geometry, add a simple rtree bounds check to
# rapidly narrow candidates down.
if query_geometry is not None:
- # Add rtree index checks to the query
- query_string += """
- AND max_x >= :min_x
- AND min_x <= :max_x
- AND max_y >= :min_y
- AND min_y <= :max_y
- """
+ # Add rtree index checks to the query.
+ # For special case of centers_within_k, Check for
+ # center of the annotation bounds within query geometry
+ # centroid + k.
+ if geometry_predicate == "centers_within_k":
+ # Use rtree index to check distance between points
+ query_string += (
+ "AND (POWER((:min_x + :max_x)/2 - (min_x + max_x)/2, 2) + "
+ " POWER((:min_y + :max_y)/2 - (min_y + max_y)/2, 2)) < :distance2 "
+ )
+ query_parameters["distance2"] = distance**2
+ # Otherwise, perform a regular bounding box intersection
+ else:
+ query_string += (
+ "AND max_x >= :min_x "
+ "AND min_x <= :max_x "
+ "AND max_y >= :min_y "
+ "AND min_y <= :max_y "
+ )
# Find the bounds of the geometry for the rtree index
min_x, min_y, max_x, max_y = query_geometry.bounds
@@ -1876,9 +2137,9 @@ def _initialize_query_string_parameters(
# The query is a full intersection check, not a simple bounds
# check only.
- if (
- geometry_predicate is not None
- and geometry_predicate != "bbox_intersects"
+ if geometry_predicate is not None and geometry_predicate not in (
+ "bbox_intersects",
+ "centers_within_k",
):
query_string += (
"\nAND geometry_predicate("
@@ -1910,6 +2171,7 @@ def _query(
no_constraints_ok: bool = False,
index_warning: bool = False,
min_area=None,
+ distance: float = 0,
) -> sqlite3.Cursor:
"""Common query construction logic for `query` and `iquery`.
@@ -1936,6 +2198,9 @@ def _query(
index_warning(bool):
Whether to warn if the query is not using an index.
Defaults to False.
+ distance (float):
+ Distance used when performing a distance based query.
+ E.g. "centers_within_k" geometry predicate.
Returns:
sqlite3.Cursor:
@@ -1964,7 +2229,12 @@ def _query(
query_parameters = {}
query_string, query_parameters = self._initialize_query_string_parameters(
- query_geometry, query_parameters, geometry_predicate, columns, where
+ query_geometry,
+ query_parameters,
+ geometry_predicate,
+ columns,
+ where,
+ distance=distance,
)
if min_area is not None and "area" in self.table_columns:
@@ -2001,6 +2271,7 @@ def iquery(
where: Optional[Predicate] = None,
geometry_predicate="intersects",
min_area=None,
+ distance: float = 0,
) -> List[str]:
"""Query the store for annotation keys.
@@ -2046,6 +2317,9 @@ def iquery(
"intersects". For more information see the `shapely
documentation on binary predicates `_.
+ distance (float):
+ Distance used when performing a distance based query.
+ E.g. "centers_within_k" geometry predicate.
Returns:
list:
@@ -2060,6 +2334,7 @@ def iquery(
where=where,
callable_columns="[key], properties",
min_area=min_area,
+ distance=distance,
)
if isinstance(where, Callable):
return [
@@ -2075,6 +2350,7 @@ def query(
where: Optional[Predicate] = None,
geometry_predicate: str = "intersects",
min_area=None,
+ distance: float = 0,
) -> Dict[str, Annotation]:
"""Runs Query."""
query_geometry = geometry
@@ -2084,6 +2360,7 @@ def query(
geometry_predicate=geometry_predicate,
where=where,
min_area=min_area,
+ distance=distance,
)
if isinstance(where, Callable):
return {
diff --git a/whitelist.txt b/whitelist.txt
index dddf955d9..2f15e7d4b 100644
--- a/whitelist.txt
+++ b/whitelist.txt
@@ -31,6 +31,7 @@ IFD
JP2
Jupyter
Kumar
+Liskov
MERCHANTABILITY
Macenko
Munkres