From 3b04eff98f0644f5752dcd7fbd7be21f6b186bdd Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Thu, 27 Jun 2024 08:49:32 +0100 Subject: [PATCH 01/16] move functions from the scripts into small package --- cyto_ml/data/intake.py | 31 ++++++++++++++++++++++++++ cyto_ml/data/s3.py | 20 +++++++++++++++++ pyproject.toml | 2 ++ scripts/intake_metadata.py | 45 ++++---------------------------------- 4 files changed, 57 insertions(+), 41 deletions(-) create mode 100644 cyto_ml/data/s3.py diff --git a/cyto_ml/data/intake.py b/cyto_ml/data/intake.py index e69de29..812adaf 100644 --- a/cyto_ml/data/intake.py +++ b/cyto_ml/data/intake.py @@ -0,0 +1,31 @@ +"""Utilities for expressing our dataset as an intake catalog""" + + +def intake_yaml( + test_url: str, + catalog_url: str, +): + """ + Write a minimal YAML template describing this as an intake datasource + Example: plankton dataset made available through scivision, metadata + https://raw.githubusercontent.com/alan-turing-institute/plankton-cefas-scivision/test_data_catalog/scivision.yml + See the comments below for decisions about its structure + """ + template = f""" +sources: + test_image: + description: Single test image from the plankton collection + origin: + driver: intake_xarray.image.ImageSource + args: + urlpath: ["{test_url}"] + exif_tags: False + plankton: + description: A CSV index of all the images of plankton + origin: + driver: intake.source.csv.CSVSource + args: + urlpath: ["{catalog_url}"] +""" + # coerce_shape: [256, 256] + return template diff --git a/cyto_ml/data/s3.py b/cyto_ml/data/s3.py new file mode 100644 index 0000000..71f72e3 --- /dev/null +++ b/cyto_ml/data/s3.py @@ -0,0 +1,20 @@ +"""Thin wrapper around the s3 object store with images and metadata""" + +import s3fs +from dotenv import load_dotenv +import os + +load_dotenv() + + +def s3_endpoint(): + """Return a reference to the object store, + reading the credentials set in the environment. + """ + fs = s3fs.S3FileSystem( + anon=False, + key=os.environ.get("FSSPEC_S3_KEY", ""), + secret=os.environ.get("FSSPEC_S3_SECRET", ""), + client_kwargs={"endpoint_url": os.environ["ENDPOINT"]}, + ) + return fs diff --git a/pyproject.toml b/pyproject.toml index deb0e18..8510474 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,3 +4,5 @@ version = "0.1" description = "This package supports the processing and analysis of plankton sample data" readme = "README.md" requires-python = "<3.10" +[tool.setuptools] +py-modules = [] diff --git a/scripts/intake_metadata.py b/scripts/intake_metadata.py index 3cddd8c..74168dc 100644 --- a/scripts/intake_metadata.py +++ b/scripts/intake_metadata.py @@ -7,46 +7,16 @@ Via https://gallery.pangeo.io/repos/pangeo-data/pangeo-tutorial-gallery/intake.html#Build-an-intake-catalog """ - -import s3fs -from dotenv import load_dotenv +from cyto_ml.data.intake import intake_yaml +from cyto_ml.data.s3 import s3_endpoint import pandas as pd import os -load_dotenv() - def load_metadata(path: str): return pd.read_csv(f"{os.environ['ENDPOINT']}/{path}") -def write_yaml(test_url: str, catalog_url: str, ): - """ - Write a minimal YAML template describing this as an intake datasource - Example: plankton dataset made available through scivision, metadata - https://raw.githubusercontent.com/alan-turing-institute/plankton-cefas-scivision/test_data_catalog/scivision.yml - See the comments below for decisions about its structure - """ - template = f""" -sources: - test_image: - description: Single test image from the plankton collection - origin: - driver: intake_xarray.image.ImageSource - args: - urlpath: ["{test_url}"] - exif_tags: False - plankton: - description: A CSV index of all the images of plankton - origin: - driver: intake.source.csv.CSVSource - args: - urlpath: ["{catalog_url}"] -""" - # coerce_shape: [256, 256] - return template - - if __name__ == "__main__": metadata = load_metadata("metadata/metadata.csv") @@ -55,14 +25,7 @@ def write_yaml(test_url: str, catalog_url: str, ): lambda x: f"{os.environ['ENDPOINT']}/untagged-images/{x}" ) - # may not need this unless we choose to write back for completeness - fs = s3fs.S3FileSystem( - anon=False, - key=os.environ.get("FSSPEC_S3_KEY", ""), - secret=os.environ.get("FSSPEC_S3_SECRET", ""), - client_kwargs={"endpoint_url": os.environ["ENDPOINT"]}, - ) - + fs = s3_endpoint() # Option to use a CSV as an index, rather than return the files catalog = "metadata/catalog.csv" with fs.open(catalog, "w") as out: @@ -85,4 +48,4 @@ def write_yaml(test_url: str, catalog_url: str, ): # * a tiny http server that creates a zip, but assumes the images have more metadata # * a tabular index instead, means we get less advantage from intake though - out.write(write_yaml(cat_test, cat_url)) + out.write(intake_yaml(cat_test, cat_url)) From 74545ae1968db63cd2543f786b37a2dceea99c30 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Thu, 27 Jun 2024 09:19:31 +0100 Subject: [PATCH 02/16] more tests, tweak lint config so it checks more --- .flake8 | 2 ++ .github/workflows/lint.yml | 2 +- cyto_ml/data/intake.py | 4 ++-- scripts/image_embeddings.py | 14 ++++++++++---- scripts/intake_metadata.py | 5 +++-- tests/conftest.py | 1 - tests/test_prepare_image.py | 9 ++++++--- tests/test_vector_store.py | 17 +++++++++++++++++ 8 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 .flake8 create mode 100644 tests/test_vector_store.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..aa079ec --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length=120 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ccef985..5568a7b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,5 +17,5 @@ jobs: uses: py-actions/flake8@v2 with: max-line-length: "120" - path: "cyto_ml" + exclude: scripts, notebooks plugins: "flake8-bugbear==22.1.11 flake8-black" \ No newline at end of file diff --git a/cyto_ml/data/intake.py b/cyto_ml/data/intake.py index 812adaf..1ea0c07 100644 --- a/cyto_ml/data/intake.py +++ b/cyto_ml/data/intake.py @@ -15,14 +15,14 @@ def intake_yaml( sources: test_image: description: Single test image from the plankton collection - origin: + origin: driver: intake_xarray.image.ImageSource args: urlpath: ["{test_url}"] exif_tags: False plankton: description: A CSV index of all the images of plankton - origin: + origin: driver: intake.source.csv.CSVSource args: urlpath: ["{catalog_url}"] diff --git a/scripts/image_embeddings.py b/scripts/image_embeddings.py index 192e6b5..b44d075 100644 --- a/scripts/image_embeddings.py +++ b/scripts/image_embeddings.py @@ -2,7 +2,12 @@ import os from dotenv import load_dotenv -from cyto_ml.models.scivision import load_model, truncate_model, prepare_image, SCIVISION_URL +from cyto_ml.models.scivision import ( + load_model, + truncate_model, + prepare_image, + SCIVISION_URL, +) from cyto_ml.data.vectorstore import vector_store from scivision import load_dataset @@ -16,7 +21,7 @@ dataset = load_dataset(f"{os.environ.get('ENDPOINT', '')}/metadata/intake.yml") - imgs = dataset.test_image().to_dask() # this will read a single image as an xarray + imgs = dataset.test_image().to_dask() # this will read a single image as an xarray vecs = vector_store() @@ -26,5 +31,6 @@ print(embeddings) - plankton = dataset.plankton().to_dask() # this will read a CSV with image locations as a dask dataframe - + plankton = ( + dataset.plankton().to_dask() + ) # this will read a CSV with image locations as a dask dataframe diff --git a/scripts/intake_metadata.py b/scripts/intake_metadata.py index 74168dc..caa6546 100644 --- a/scripts/intake_metadata.py +++ b/scripts/intake_metadata.py @@ -3,10 +3,11 @@ https://scivision.readthedocs.io/en/latest/api.html#scivision.io.reader.load_dataset https://intake.readthedocs.io/en/latest/catalog.html#yaml-format -See also https://github.com/intake/intake-stac +See also https://github.com/intake/intake-stac Via https://gallery.pangeo.io/repos/pangeo-data/pangeo-tutorial-gallery/intake.html#Build-an-intake-catalog """ + from cyto_ml.data.intake import intake_yaml from cyto_ml.data.s3 import s3_endpoint import pandas as pd @@ -37,7 +38,7 @@ def load_metadata(path: str): # out.write(write_yaml(f"{os.environ['ENDPOINT']}/{catalog}")) # All the scivision examples have image collections in a single zipfile - # This format throws an s3 error on the directory listing - + # This format throws an s3 error on the directory listing - # unsure if this is a permissions issue, or you just can't use a wildcard cat_wildcard = f"{os.environ['ENDPOINT']}/untagged-images/*.tif" # .replace('https://', 's3://') diff --git a/tests/conftest.py b/tests/conftest.py index eefe92a..3d8cae3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ import pytest - @pytest.fixture def image_dir(): """ diff --git a/tests/test_prepare_image.py b/tests/test_prepare_image.py index c9426c9..13888f1 100644 --- a/tests/test_prepare_image.py +++ b/tests/test_prepare_image.py @@ -1,6 +1,7 @@ # test_prepare_image.py import pytest import torch +import logging from intake_xarray import ImageSource from cyto_ml.models.scivision import prepare_image @@ -19,15 +20,17 @@ def test_single_image(single_image): def test_image_batch(image_batch): """ - Currently expected to fail because dask wants images to share dimensions + Currently expected to fail because dask wants images to share dimensions, ours don't + Needs digging into the (source) data from the FlowCam that gets decollaged + We either pad them (and process a lot of blank space) or stick to single image input """ # Load a batch of plankton images image_data = ImageSource(image_batch).to_dask() with pytest.raises(ValueError) as err: - prepared_batch = prepare_image(image_data) - print(err) + _ = prepare_image(image_data) + logging.info(err) # Check if the shape is correct # assert prepared_batch.shape == torch.Size([64, 89, 36, 3]) diff --git a/tests/test_vector_store.py b/tests/test_vector_store.py new file mode 100644 index 0000000..657a687 --- /dev/null +++ b/tests/test_vector_store.py @@ -0,0 +1,17 @@ +from cyto_ml.data.vectorstore import vector_store +import numpy as np + + +def test_store(): + store = vector_store() # default 'test_collection' + id = "id_1" # insists on a str + filename = "https://example.com/filename.tif" + store.add( + documents=[filename], # we use image location in s3 rather than text content + embeddings=[list(np.random.rand(2048))], # wants a list of lists + ids=[id], + ) # wants a list of ids + + record = store.get("id_1", include=["embeddings"]) + assert record + assert len(record["embeddings"][0]) == 2048 From 9d7e3c61750194d356955fd9571924034b61459d Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Thu, 27 Jun 2024 10:00:56 +0100 Subject: [PATCH 03/16] lint action throws error with the default '.' flake8 path --- .github/workflows/lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5568a7b..1144ba8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,5 +17,6 @@ jobs: uses: py-actions/flake8@v2 with: max-line-length: "120" + path: ./ exclude: scripts, notebooks plugins: "flake8-bugbear==22.1.11 flake8-black" \ No newline at end of file From 96cc2339c42b022f05c06b7b1607775e66533870 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Thu, 27 Jun 2024 10:58:00 +0100 Subject: [PATCH 04/16] try a comma separated flake8 path, then either give up or move the tests --- .github/workflows/lint.yml | 3 +-- .gitignore | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1144ba8..8769438 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,6 +17,5 @@ jobs: uses: py-actions/flake8@v2 with: max-line-length: "120" - path: ./ - exclude: scripts, notebooks + path: cyto_ml, tests plugins: "flake8-bugbear==22.1.11 flake8-black" \ No newline at end of file diff --git a/.gitignore b/.gitignore index aabb804..274819e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env **/.ipynb_checkpoints/ **/__pycache__/ +vectors/ From 0466cd6155a164c70ed4454fb14fe01e2b919f01 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Thu, 27 Jun 2024 10:59:39 +0100 Subject: [PATCH 05/16] move tests inside package and put lint action back how it was --- .github/workflows/lint.yml | 2 +- {tests => cyto_ml/tests}/conftest.py | 0 .../fixtures/test_images/testymctestface_1091.tif | Bin .../fixtures/test_images/testymctestface_113.tif | Bin .../fixtures/test_images/testymctestface_127.tif | Bin .../fixtures/test_images/testymctestface_133.tif | Bin .../fixtures/test_images/testymctestface_1388.tif | Bin .../fixtures/test_images/testymctestface_1407.tif | Bin .../fixtures/test_images/testymctestface_1830.tif | Bin .../fixtures/test_images/testymctestface_1876.tif | Bin .../fixtures/test_images/testymctestface_188.tif | Bin .../fixtures/test_images/testymctestface_1887.tif | Bin .../fixtures/test_images/testymctestface_1890.tif | Bin .../fixtures/test_images/testymctestface_1892.tif | Bin .../fixtures/test_images/testymctestface_1901.tif | Bin .../fixtures/test_images/testymctestface_1909.tif | Bin .../fixtures/test_images/testymctestface_1912.tif | Bin .../fixtures/test_images/testymctestface_1914.tif | Bin .../fixtures/test_images/testymctestface_1915.tif | Bin .../fixtures/test_images/testymctestface_1919.tif | Bin .../fixtures/test_images/testymctestface_1922.tif | Bin .../fixtures/test_images/testymctestface_1924.tif | Bin .../fixtures/test_images/testymctestface_1948.tif | Bin .../fixtures/test_images/testymctestface_1953.tif | Bin .../fixtures/test_images/testymctestface_1962.tif | Bin .../fixtures/test_images/testymctestface_1965.tif | Bin .../fixtures/test_images/testymctestface_1981.tif | Bin .../fixtures/test_images/testymctestface_2012.tif | Bin .../fixtures/test_images/testymctestface_2071.tif | Bin .../fixtures/test_images/testymctestface_2102.tif | Bin .../fixtures/test_images/testymctestface_2108.tif | Bin .../fixtures/test_images/testymctestface_2110.tif | Bin .../fixtures/test_images/testymctestface_2115.tif | Bin .../fixtures/test_images/testymctestface_2117.tif | Bin .../fixtures/test_images/testymctestface_2119.tif | Bin .../fixtures/test_images/testymctestface_2172.tif | Bin .../fixtures/test_images/testymctestface_2715.tif | Bin .../fixtures/test_images/testymctestface_36.tif | Bin .../fixtures/test_images/testymctestface_3612.tif | Bin .../fixtures/test_images/testymctestface_3814.tif | Bin .../fixtures/test_images/testymctestface_4715.tif | Bin .../fixtures/test_images/testymctestface_4961.tif | Bin {tests => cyto_ml/tests}/test_prepare_image.py | 0 {tests => cyto_ml/tests}/test_vector_store.py | 0 44 files changed, 1 insertion(+), 1 deletion(-) rename {tests => cyto_ml/tests}/conftest.py (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1091.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_113.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_127.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_133.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1388.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1407.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1830.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1876.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_188.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1887.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1890.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1892.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1901.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1909.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1912.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1914.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1915.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1919.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1922.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1924.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1948.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1953.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1962.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1965.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_1981.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2012.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2071.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2102.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2108.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2110.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2115.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2117.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2119.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2172.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_2715.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_36.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_3612.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_3814.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_4715.tif (100%) rename {tests => cyto_ml/tests}/fixtures/test_images/testymctestface_4961.tif (100%) rename {tests => cyto_ml/tests}/test_prepare_image.py (100%) rename {tests => cyto_ml/tests}/test_vector_store.py (100%) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8769438..f22f642 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,5 +17,5 @@ jobs: uses: py-actions/flake8@v2 with: max-line-length: "120" - path: cyto_ml, tests + path: cyto_ml plugins: "flake8-bugbear==22.1.11 flake8-black" \ No newline at end of file diff --git a/tests/conftest.py b/cyto_ml/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to cyto_ml/tests/conftest.py diff --git a/tests/fixtures/test_images/testymctestface_1091.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1091.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1091.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1091.tif diff --git a/tests/fixtures/test_images/testymctestface_113.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_113.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_113.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_113.tif diff --git a/tests/fixtures/test_images/testymctestface_127.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_127.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_127.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_127.tif diff --git a/tests/fixtures/test_images/testymctestface_133.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_133.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_133.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_133.tif diff --git a/tests/fixtures/test_images/testymctestface_1388.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1388.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1388.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1388.tif diff --git a/tests/fixtures/test_images/testymctestface_1407.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1407.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1407.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1407.tif diff --git a/tests/fixtures/test_images/testymctestface_1830.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1830.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1830.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1830.tif diff --git a/tests/fixtures/test_images/testymctestface_1876.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1876.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1876.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1876.tif diff --git a/tests/fixtures/test_images/testymctestface_188.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_188.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_188.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_188.tif diff --git a/tests/fixtures/test_images/testymctestface_1887.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1887.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1887.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1887.tif diff --git a/tests/fixtures/test_images/testymctestface_1890.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1890.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1890.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1890.tif diff --git a/tests/fixtures/test_images/testymctestface_1892.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1892.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1892.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1892.tif diff --git a/tests/fixtures/test_images/testymctestface_1901.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1901.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1901.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1901.tif diff --git a/tests/fixtures/test_images/testymctestface_1909.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1909.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1909.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1909.tif diff --git a/tests/fixtures/test_images/testymctestface_1912.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1912.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1912.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1912.tif diff --git a/tests/fixtures/test_images/testymctestface_1914.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1914.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1914.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1914.tif diff --git a/tests/fixtures/test_images/testymctestface_1915.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1915.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1915.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1915.tif diff --git a/tests/fixtures/test_images/testymctestface_1919.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1919.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1919.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1919.tif diff --git a/tests/fixtures/test_images/testymctestface_1922.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1922.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1922.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1922.tif diff --git a/tests/fixtures/test_images/testymctestface_1924.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1924.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1924.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1924.tif diff --git a/tests/fixtures/test_images/testymctestface_1948.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1948.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1948.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1948.tif diff --git a/tests/fixtures/test_images/testymctestface_1953.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1953.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1953.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1953.tif diff --git a/tests/fixtures/test_images/testymctestface_1962.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1962.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1962.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1962.tif diff --git a/tests/fixtures/test_images/testymctestface_1965.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1965.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1965.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1965.tif diff --git a/tests/fixtures/test_images/testymctestface_1981.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_1981.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_1981.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_1981.tif diff --git a/tests/fixtures/test_images/testymctestface_2012.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2012.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2012.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2012.tif diff --git a/tests/fixtures/test_images/testymctestface_2071.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2071.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2071.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2071.tif diff --git a/tests/fixtures/test_images/testymctestface_2102.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2102.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2102.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2102.tif diff --git a/tests/fixtures/test_images/testymctestface_2108.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2108.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2108.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2108.tif diff --git a/tests/fixtures/test_images/testymctestface_2110.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2110.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2110.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2110.tif diff --git a/tests/fixtures/test_images/testymctestface_2115.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2115.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2115.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2115.tif diff --git a/tests/fixtures/test_images/testymctestface_2117.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2117.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2117.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2117.tif diff --git a/tests/fixtures/test_images/testymctestface_2119.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2119.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2119.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2119.tif diff --git a/tests/fixtures/test_images/testymctestface_2172.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2172.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2172.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2172.tif diff --git a/tests/fixtures/test_images/testymctestface_2715.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_2715.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_2715.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_2715.tif diff --git a/tests/fixtures/test_images/testymctestface_36.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_36.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_36.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_36.tif diff --git a/tests/fixtures/test_images/testymctestface_3612.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_3612.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_3612.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_3612.tif diff --git a/tests/fixtures/test_images/testymctestface_3814.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_3814.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_3814.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_3814.tif diff --git a/tests/fixtures/test_images/testymctestface_4715.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_4715.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_4715.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_4715.tif diff --git a/tests/fixtures/test_images/testymctestface_4961.tif b/cyto_ml/tests/fixtures/test_images/testymctestface_4961.tif similarity index 100% rename from tests/fixtures/test_images/testymctestface_4961.tif rename to cyto_ml/tests/fixtures/test_images/testymctestface_4961.tif diff --git a/tests/test_prepare_image.py b/cyto_ml/tests/test_prepare_image.py similarity index 100% rename from tests/test_prepare_image.py rename to cyto_ml/tests/test_prepare_image.py diff --git a/tests/test_vector_store.py b/cyto_ml/tests/test_vector_store.py similarity index 100% rename from tests/test_vector_store.py rename to cyto_ml/tests/test_vector_store.py From ebf9546ba059ffae6157fefddf00dfc7271554f7 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 09:40:23 +0100 Subject: [PATCH 06/16] Explicit opt out of chromadb telemetry --- cyto_ml/data/vectorstore.py | 9 ++++++++- cyto_ml/tests/test_vector_store.py | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cyto_ml/data/vectorstore.py b/cyto_ml/data/vectorstore.py index ed0e9bf..78e77f6 100644 --- a/cyto_ml/data/vectorstore.py +++ b/cyto_ml/data/vectorstore.py @@ -1,11 +1,18 @@ import chromadb from chromadb.db.base import UniqueConstraintError +from chromadb.config import Settings + from typing import Optional import logging logging.basicConfig(level=logging.INFO) -client = chromadb.PersistentClient(path="./vectors") +client = chromadb.PersistentClient( + path="./vectors", + settings=Settings( + anonymized_telemetry=False, + ), +) def vector_store(name: Optional[str] = "test_collection"): diff --git a/cyto_ml/tests/test_vector_store.py b/cyto_ml/tests/test_vector_store.py index 657a687..983c632 100644 --- a/cyto_ml/tests/test_vector_store.py +++ b/cyto_ml/tests/test_vector_store.py @@ -1,7 +1,11 @@ -from cyto_ml.data.vectorstore import vector_store +from cyto_ml.data.vectorstore import vector_store, client import numpy as np +def test_client_no_telemetry(): + assert not client.get_settings()["anonymized_telemetry"] + + def test_store(): store = vector_store() # default 'test_collection' id = "id_1" # insists on a str From 334f552a1ee3505dbd764656d9c999ae4ce15841 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 10:41:23 +0100 Subject: [PATCH 07/16] replace the image metadata with file listing as per #4 --- scripts/intake_metadata.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/scripts/intake_metadata.py b/scripts/intake_metadata.py index caa6546..ec5fe6c 100644 --- a/scripts/intake_metadata.py +++ b/scripts/intake_metadata.py @@ -1,4 +1,4 @@ -"""Convert the metadata into format usable with `intake`, +"""Heavy-handed approach to create image metadata in usable with `intake`, for trial use with `scivision`: https://scivision.readthedocs.io/en/latest/api.html#scivision.io.reader.load_dataset https://intake.readthedocs.io/en/latest/catalog.html#yaml-format @@ -10,40 +10,39 @@ from cyto_ml.data.intake import intake_yaml from cyto_ml.data.s3 import s3_endpoint +from s3fs import S3FileSystem import pandas as pd import os -def load_metadata(path: str): - return pd.read_csv(f"{os.environ['ENDPOINT']}/{path}") +def image_index(endpoint: S3FileSystem, location: str): + """Find and likely later filter records in a bucket""" + index = endpoint.ls(location) + return pd.DataFrame( + [f"{os.environ['ENDPOINT']}/untagged-images/{x}" for x in index], + columns=["Filename"], + ) if __name__ == "__main__": - metadata = load_metadata("metadata/metadata.csv") - - # rewrite it to add the full s3 image path - metadata["Filename"] = metadata["Filename"].apply( - lambda x: f"{os.environ['ENDPOINT']}/untagged-images/{x}" - ) fs = s3_endpoint() + metadata = image_index(fs, "untagged-images") + # Option to use a CSV as an index, rather than return the files catalog = "metadata/catalog.csv" with fs.open(catalog, "w") as out: - out.write(metadata.to_csv()) + out.write(metadata.to_csv(index=False)) + cat_url = f"{os.environ['ENDPOINT']}/{catalog}" with fs.open("metadata/intake.yml", "w") as out: # Do we use a CSV driver and include the metadata? # out.write(write_yaml(f"{os.environ['ENDPOINT']}/{catalog}")) - # All the scivision examples have image collections in a single zipfile - # This format throws an s3 error on the directory listing - - # unsure if this is a permissions issue, or you just can't use a wildcard - cat_wildcard = f"{os.environ['ENDPOINT']}/untagged-images/*.tif" # .replace('https://', 's3://') - - # Create a testing record for a single file - cat_test = cat_wildcard.replace("*", "19_10_Tank22_1") + # See the issue here: https://github.com/NERC-CEH/plankton_ml/issues/3 + # About data improvements needed before a better way to read a bucket into s3 + cat_test = f"{os.environ['ENDPOINT']}/untagged-images/19_10_Tank22_1.tif" # Our options for the whole collection look like: # * a tiny http server that creates a zip, but assumes the images have more metadata From 6d503e7ad5a5b1d42d2d197d6ac96290d21e5c36 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 11:00:45 +0100 Subject: [PATCH 08/16] correct markdown bracket order --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 135ccc9..b5182a8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ conda create -n cyto_39 python=3.9 conda env update ``` -Please note that this is specifically pinned to python 3.9 due to dependency versions; we make experimental use of the [https://sci.vision/#/model/resnet50-plankton](CEFAS plankton model available through SciVision), which in turn uses an older version of pytorch that isn't packaged above python 3.9. +Please note that this is specifically pinned to python 3.9 due to dependency versions; we make experimental use of the [CEFAS plankton model available through SciVision](https://sci.vision/#/model/resnet50-plankton), which in turn uses an older version of pytorch that isn't packaged above python 3.9. ### Object store connection @@ -40,7 +40,7 @@ Get started by cloning this repository and running ### Feature extraction -Experiment testing workflows by using [https://sci.vision/#/model/resnet50-plankton](this plankton model from SciVision) to extract features from images for use in similarity search, clustering, etc. +Experiment testing workflows by using [this plankton model from SciVision](https://sci.vision/#/model/resnet50-plankton) to extract features from images for use in similarity search, clustering, etc. ### TBC (object store upload, derived classifiers, etc) From c8150b8eef9a106c7c56dceb194218b4c3a5e633 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 14:00:12 +0100 Subject: [PATCH 09/16] utils and tests for searchable image features --- cyto_ml/models/scivision.py | 6 ++++++ cyto_ml/tests/conftest.py | 10 ++++++++++ cyto_ml/tests/test_image_embeddings.py | 13 +++++++++++++ cyto_ml/tests/test_prepare_image.py | 2 +- scripts/intake_metadata.py | 2 +- 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 cyto_ml/tests/test_image_embeddings.py diff --git a/cyto_ml/models/scivision.py b/cyto_ml/models/scivision.py index 3de3ea8..2aa4de3 100644 --- a/cyto_ml/models/scivision.py +++ b/cyto_ml/models/scivision.py @@ -49,3 +49,9 @@ def prepare_image(image: DataArray): tensor_image = tensor_image.cuda() return tensor_image + + +def flat_embeddings(features: torch.Tensor): + """Utility function that takes the features returned by the model in truncate_model + And flattens them into a list suitable for storing in a vector database""" + return list(features[0].squeeze(1).squeeze(1).detach().numpy().astype(float)) diff --git a/cyto_ml/tests/conftest.py b/cyto_ml/tests/conftest.py index 3d8cae3..6582952 100644 --- a/cyto_ml/tests/conftest.py +++ b/cyto_ml/tests/conftest.py @@ -1,5 +1,10 @@ import os import pytest +from cyto_ml.models.scivision import ( + load_model, + truncate_model, + SCIVISION_URL, +) @pytest.fixture @@ -21,3 +26,8 @@ def single_image(image_dir): @pytest.fixture def image_batch(image_dir): return os.path.join(image_dir, "testymctestface_*.tif") + + +@pytest.fixture +def scivision_model(): + return truncate_model(load_model(SCIVISION_URL)) diff --git a/cyto_ml/tests/test_image_embeddings.py b/cyto_ml/tests/test_image_embeddings.py new file mode 100644 index 0000000..58263b8 --- /dev/null +++ b/cyto_ml/tests/test_image_embeddings.py @@ -0,0 +1,13 @@ +from intake_xarray import ImageSource +from torch import Tensor +from cyto_ml.models.scivision import prepare_image, flat_embeddings + + +def test_embeddings(scivision_model, single_image): + features = scivision_model(prepare_image(ImageSource(single_image).to_dask())) + + assert isinstance(features, Tensor) + + embeddings = flat_embeddings(features) + + assert len(embeddings) == features.size()[1] diff --git a/cyto_ml/tests/test_prepare_image.py b/cyto_ml/tests/test_prepare_image.py index 13888f1..459496b 100644 --- a/cyto_ml/tests/test_prepare_image.py +++ b/cyto_ml/tests/test_prepare_image.py @@ -11,7 +11,7 @@ def test_single_image(single_image): image_data = ImageSource(single_image).to_dask() - # Prepare the image + # Tensorise the image (potentially normalise if we have useful values) prepared_image = prepare_image(image_data) # Check if the shape is correct (batch dimension added) diff --git a/scripts/intake_metadata.py b/scripts/intake_metadata.py index ec5fe6c..5e384c2 100644 --- a/scripts/intake_metadata.py +++ b/scripts/intake_metadata.py @@ -19,7 +19,7 @@ def image_index(endpoint: S3FileSystem, location: str): """Find and likely later filter records in a bucket""" index = endpoint.ls(location) return pd.DataFrame( - [f"{os.environ['ENDPOINT']}/untagged-images/{x}" for x in index], + [f"{os.environ['ENDPOINT']}/{x}" for x in index], columns=["Filename"], ) From dec30c808dfc279a6963e61993c39e39dfacd062 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 14:10:48 +0100 Subject: [PATCH 10/16] adapt script to run through the whole image collection --- scripts/image_embeddings.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/scripts/image_embeddings.py b/scripts/image_embeddings.py index b44d075..72a3ee1 100644 --- a/scripts/image_embeddings.py +++ b/scripts/image_embeddings.py @@ -6,10 +6,12 @@ load_model, truncate_model, prepare_image, + flat_embeddings, SCIVISION_URL, ) from cyto_ml.data.vectorstore import vector_store from scivision import load_dataset +from intake_xarray import ImageSource load_dotenv() @@ -20,17 +22,28 @@ # https://github.com/AnnaLinton/scivision_examples/blob/main/how-to-use-scivision.ipynb dataset = load_dataset(f"{os.environ.get('ENDPOINT', '')}/metadata/intake.yml") - - imgs = dataset.test_image().to_dask() # this will read a single image as an xarray - - vecs = vector_store() + collection = vector_store("plankton") model = truncate_model(load_model(SCIVISION_URL)) - embeddings = model(prepare_image(imgs)) - - print(embeddings) - plankton = ( - dataset.plankton().to_dask() + dataset.plankton().to_dask().compute() ) # this will read a CSV with image locations as a dask dataframe + + # Feels like this is doing dask wrong, compute() should happen later + # If it doesn't, there are complaints about meta= return value inference + # that suggest this is wrongheaded use of `apply`: need to learn better patterns + # So this is a kludge, but we're still very much in prototype territory - + # Come back and refine this if the next parts work! + + def store_embeddings(row): + image_data = ImageSource(row.Filename).to_dask() + embeddings = flat_embeddings(model(prepare_image(image_data))) + collection.add( + documents=[row.Filename], + embeddings=[embeddings], + ids=[row.Filename], # must be unique + # Note - optional arg name is "metadatas" (we don't have any) + ) + + plankton.apply(store_embeddings, axis=1) From d0d0e96fea3ffb6a3f93391a95a8b73fc309397a Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 14:28:48 +0100 Subject: [PATCH 11/16] catch an intermittent read error, TODO for it later --- scripts/image_embeddings.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/image_embeddings.py b/scripts/image_embeddings.py index 72a3ee1..d157b63 100644 --- a/scripts/image_embeddings.py +++ b/scripts/image_embeddings.py @@ -1,6 +1,7 @@ """Try to use the scivision pretrained model and tools against this collection""" import os +import logging from dotenv import load_dotenv from cyto_ml.models.scivision import ( load_model, @@ -13,6 +14,7 @@ from scivision import load_dataset from intake_xarray import ImageSource +logging.basicConfig(level=logging.info) load_dotenv() @@ -37,8 +39,20 @@ # Come back and refine this if the next parts work! def store_embeddings(row): - image_data = ImageSource(row.Filename).to_dask() + try: + image_data = ImageSource(row.Filename).to_dask() + except ValueError as err: + # TODO diagnose and fix for this happening, in rare circumstances: + # (would be nice to know rather than just buffer the image and add code) + # File "python3.9/site-packages/PIL/PcdImagePlugin.py", line 34, in _open + # self.fp.seek(2048) + # File "python3.9/site-packages/fsspec/implementations/http.py", line 745, in seek + # raise ValueError("Cannot seek streaming HTTP file") + logging.info(err) + return + embeddings = flat_embeddings(model(prepare_image(image_data))) + collection.add( documents=[row.Filename], embeddings=[embeddings], From ee56814144a2aa01cad55c263266a9eabcb4fec1 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 14:30:46 +0100 Subject: [PATCH 12/16] belatedly log the problematic filenames --- scripts/image_embeddings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/image_embeddings.py b/scripts/image_embeddings.py index d157b63..0906b48 100644 --- a/scripts/image_embeddings.py +++ b/scripts/image_embeddings.py @@ -49,6 +49,7 @@ def store_embeddings(row): # File "python3.9/site-packages/fsspec/implementations/http.py", line 745, in seek # raise ValueError("Cannot seek streaming HTTP file") logging.info(err) + logging.info(row.Filename) return embeddings = flat_embeddings(model(prepare_image(image_data))) From 5c0c11b1a448a3d5390600c5129c81fb8a21fadc Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 14:40:57 +0100 Subject: [PATCH 13/16] stub notebook for feature search / cluster with notes on aims --- notebooks/VectorSearch.ipynb | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 notebooks/VectorSearch.ipynb diff --git a/notebooks/VectorSearch.ipynb b/notebooks/VectorSearch.ipynb new file mode 100644 index 0000000..d8c5b3e --- /dev/null +++ b/notebooks/VectorSearch.ipynb @@ -0,0 +1,49 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('../')\n", + "from cyto_ml.data.vectorstore import vector_store" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By now we should have a vector db (chromadb for now, sqlite3 under the hood) full of 2048-long lists of image embeddings.\n", + "What can we hope to drop out of them?\n", + "\n", + "* Image similarity search (either on an image in this collection, or an unseen one)\n", + "* Different self-supervised clustering methods (nice reference here: https://sslneurips23.github.io/paper_pdfs/paper_70.pdf)\n", + "\n", + "What are the outcomes we are looking for here (if any of this in fact works?)\n", + "\n", + "* Insights into assemblies of functional traits without having to do much taxonomy, very open-ended\n", + "* Ability to train a cheap-to-run binary classifier (plankton or not) that reliably filters data before it goes into object storage, without having to develop many rules\n", + "* Ability to gauge how well an off the shelf model is able to discriminate our data (assess the value of doing a lot of labelling for a custom model)\n", + "* Utility for assisted labelling without having to do _much_ ML (e.g. similarity search to autosuggest attributes based on colocated clusters)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "store = vector_store()" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 2b02cc465d0c8b018621365b7f0a898210ab910b Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 15:49:42 +0100 Subject: [PATCH 14/16] bypass relative file paths for vector storage, sigh --- cyto_ml/data/vectorstore.py | 10 +- notebooks/ImageEmbeddings.ipynb | 3679 ++++++++++--------------------- 2 files changed, 1204 insertions(+), 2485 deletions(-) diff --git a/cyto_ml/data/vectorstore.py b/cyto_ml/data/vectorstore.py index 78e77f6..f6e5bcc 100644 --- a/cyto_ml/data/vectorstore.py +++ b/cyto_ml/data/vectorstore.py @@ -1,14 +1,18 @@ +import os +from typing import Optional +import logging + import chromadb from chromadb.db.base import UniqueConstraintError from chromadb.config import Settings -from typing import Optional -import logging logging.basicConfig(level=logging.INFO) +# TODO make this sensibly configurable, not confusingly hardcoded +STORE = os.path.join(os.path.abspath(os.path.dirname(__file__)), "../../vectors") client = chromadb.PersistentClient( - path="./vectors", + path=STORE, settings=Settings( anonymized_telemetry=False, ), diff --git a/notebooks/ImageEmbeddings.ipynb b/notebooks/ImageEmbeddings.ipynb index c13cd5a..c35691b 100644 --- a/notebooks/ImageEmbeddings.ipynb +++ b/notebooks/ImageEmbeddings.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 109, + "execution_count": 34, "metadata": {}, "outputs": [ { @@ -21,7 +21,7 @@ "True" ] }, - "execution_count": 109, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -33,14 +33,28 @@ "import torch\n", "import torchvision\n", "import chromadb\n", + "import sys\n", + "sys.path.append('../')\n", + "from cyto_ml.models.scivision import prepare_image\n", + "from intake_xarray import ImageSource\n", "load_dotenv() # sets our object store endpoint and credentials from the .env file" ] }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 26, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/jowals/miniconda3/envs/cyto_39/lib/python3.9/site-packages/xarray/core/dataarray.py:1399: FutureWarning: None value for 'chunks' is deprecated. It will raise an error in the future. Use instead '{}'\n", + " warnings.warn(\n", + "/home/jowals/miniconda3/envs/cyto_39/lib/python3.9/site-packages/intake_xarray/image.py:474: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.\n", + " 'dims': dict(ds2.dims),\n" + ] + }, { "data": { "text/html": [ @@ -412,7 +426,7 @@ "Coordinates:\n", " * y (y) int64 192B 0 1 2 3 4 5 6 7 8 9 ... 15 16 17 18 19 20 21 22 23\n", " * x (x) int64 120B 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14\n", - " * channel (channel) int64 24B 0 1 2" + " dtype='int64', name='y'))
  • x
    PandasIndex
    PandasIndex(Index([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], dtype='int64', name='x'))
  • channel
    PandasIndex
    PandasIndex(Index([0, 1, 2], dtype='int64', name='channel'))
  • " ], "text/plain": [ " Size: 1kB\n", @@ -504,7 +518,7 @@ " * channel (channel) int64 24B 0 1 2" ] }, - "execution_count": 67, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -539,2460 +553,90 @@ "A quick look at the example dataset that comes with the model, for reference" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this case we don't want to use the `predict` interface anyway (one of N class labels) - we want the features that go into the last fully-connected layer (as described here https://stackoverflow.com/a/52548419)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "network = torch.nn.Sequential(*(list(model._plumbing.model.pretrained_model.children())[:-1]))" + ] + }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 9, "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "sources:\n", - " plankton:\n", - " description: Load example images of plankton from COPEPEDIA public dataset\n", - " origin: \n", - " driver: intake_xarray.image.ImageSource\n", - " args:\n", - " urlpath: [\"zip://*.tif::https://zenodo.org/record/6143685/files/images.zip\"]\n", - " chunks: {}\n", - " storage_options: {'anon': True}\n", - " coerce_shape: [1000, 1000]\n", - " exif_tags: True\n", - "\n" - ] + "data": { + "text/plain": [ + "(24, 15, 3)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "import requests\n", - "print(requests.get(target_datasource.url.item()).text)" + "imgs = dataset.test_image().to_dask()\n", + "i= imgs.to_numpy()\n", + "i.shape\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "https://github.com/alan-turing-institute/plankton-cefas-scivision/blob/main/resnet50_cefas/data.py \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pass the image through our truncated network and get some embeddings out" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/home/jowals/miniconda3/envs/cyto_39/lib/python3.9/site-packages/intake_xarray/image.py:474: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.\n", - " 'dims': dict(ds2.dims),\n" + "[W NNPACK.cpp:79] Could not initialize NNPACK! Reason: Unsupported hardware.\n" ] }, - { - "data": { - "text/html": [ - "
    \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
    <xarray.Dataset> Size: 78MB\n",
    -       "Dimensions:                               (concat_dim: 26, y: 1000, x: 1000,\n",
    -       "                                           channel: 3)\n",
    -       "Coordinates:\n",
    -       "  * concat_dim                            (concat_dim) int64 208B 0 1 ... 24 25\n",
    -       "  * y                                     (y) int64 8kB 0 1 2 3 ... 997 998 999\n",
    -       "  * x                                     (x) int64 8kB 0 1 2 3 ... 997 998 999\n",
    -       "  * channel                               (channel) int64 24B 0 1 2\n",
    -       "Data variables: (12/23)\n",
    -       "    raster                                (concat_dim, y, x, channel) uint8 78MB dask.array<chunksize=(1, 1000, 1000, 3), meta=np.ndarray>\n",
    -       "    EXIF Image ImageWidth                 (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    EXIF Image ImageLength                (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    EXIF Image BitsPerSample              (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    EXIF Image Compression                (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    EXIF Image PhotometricInterpretation  (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    ...                                    ...\n",
    -       "    EXIF GPS GPSVersionID                 (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    EXIF GPS GPSLatitudeRef               (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    EXIF GPS GPSLatitude                  (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    EXIF GPS GPSLongitudeRef              (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    EXIF GPS GPSLongitude                 (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>\n",
    -       "    EXIF Image GPSInfo                    (concat_dim) object 208B dask.array<chunksize=(1,), meta=np.ndarray>
    " - ], - "text/plain": [ - " Size: 78MB\n", - "Dimensions: (concat_dim: 26, y: 1000, x: 1000,\n", - " channel: 3)\n", - "Coordinates:\n", - " * concat_dim (concat_dim) int64 208B 0 1 ... 24 25\n", - " * y (y) int64 8kB 0 1 2 3 ... 997 998 999\n", - " * x (x) int64 8kB 0 1 2 3 ... 997 998 999\n", - " * channel (channel) int64 24B 0 1 2\n", - "Data variables: (12/23)\n", - " raster (concat_dim, y, x, channel) uint8 78MB dask.array\n", - " EXIF Image ImageWidth (concat_dim) object 208B dask.array\n", - " EXIF Image ImageLength (concat_dim) object 208B dask.array\n", - " EXIF Image BitsPerSample (concat_dim) object 208B dask.array\n", - " EXIF Image Compression (concat_dim) object 208B dask.array\n", - " EXIF Image PhotometricInterpretation (concat_dim) object 208B dask.array\n", - " ... ...\n", - " EXIF GPS GPSVersionID (concat_dim) object 208B dask.array\n", - " EXIF GPS GPSLatitudeRef (concat_dim) object 208B dask.array\n", - " EXIF GPS GPSLatitude (concat_dim) object 208B dask.array\n", - " EXIF GPS GPSLongitudeRef (concat_dim) object 208B dask.array\n", - " EXIF GPS GPSLongitude (concat_dim) object 208B dask.array\n", - " EXIF Image GPSInfo (concat_dim) object 208B dask.array" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from scivision.catalog import default_catalog\n", - "model_name = 'resnet50-plankton'\n", - "compatible_datasources = default_catalog.compatible_datasources(model_name).to_dataframe()\n", - "target_datasource = compatible_datasources.loc[compatible_datasources['name'] == 'cefas-plankton']\n", - "cat = load_dataset(target_datasource.url.item()) \n", - "dataset = cat.plankton().to_dask()\n", - "dataset\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this case we don't want to use the `predict` interface anyway (one of N class labels) - we want the features that go into the last fully-connected layer (as described here https://stackoverflow.com/a/52548419)" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [], - "source": [ - "network = torch.nn.Sequential(*(list(model._plumbing.model.pretrained_model.children())[:-1]))" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Sequential(\n", - " (0): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n", - " (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (2): ReLU(inplace=True)\n", - " (3): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n", - " (4): Sequential(\n", - " (0): Bottleneck(\n", - " (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " (downsample): Sequential(\n", - " (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " )\n", - " )\n", - " (1): Bottleneck(\n", - " (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " (2): Bottleneck(\n", - " (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " )\n", - " (5): Sequential(\n", - " (0): Bottleneck(\n", - " (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " (downsample): Sequential(\n", - " (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n", - " (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " )\n", - " )\n", - " (1): Bottleneck(\n", - " (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " (2): Bottleneck(\n", - " (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " (3): Bottleneck(\n", - " (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " )\n", - " (6): Sequential(\n", - " (0): Bottleneck(\n", - " (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " (downsample): Sequential(\n", - " (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n", - " (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " )\n", - " )\n", - " (1): Bottleneck(\n", - " (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " (2): Bottleneck(\n", - " (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " (3): Bottleneck(\n", - " (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " (4): Bottleneck(\n", - " (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " (5): Bottleneck(\n", - " (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " )\n", - " (7): Sequential(\n", - " (0): Bottleneck(\n", - " (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " (downsample): Sequential(\n", - " (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n", - " (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " )\n", - " )\n", - " (1): Bottleneck(\n", - " (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " (2): Bottleneck(\n", - " (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n", - " (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n", - " (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n", - " (relu): ReLU(inplace=True)\n", - " )\n", - " )\n", - " (8): AdaptiveAvgPool2d(output_size=(1, 1))\n", - ")" - ] - }, - "execution_count": 50, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "network\n" - ] - }, - { - "cell_type": "code", - "execution_count": 92, - "metadata": {}, - "outputs": [ { "data": { "text/plain": [ - "(24, 15, 3)" + "torch.Size([1, 2048, 1, 1])" ] }, - "execution_count": 92, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "imgs = dataset.test_image().to_dask()\n", - "i= imgs.to_numpy()\n", - "i.shape\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "https://github.com/alan-turing-institute/plankton-cefas-scivision/blob/main/resnet50_cefas/data.py \n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Pass the image through our truncated network and get some embeddings out" + "o = torch.stack([torchvision.transforms.ToTensor()(i)])\n", + "feats = network(o)\n", + "feats.shape" ] }, { "cell_type": "code", - "execution_count": 154, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -3001,7 +645,7 @@ }, { "cell_type": "code", - "execution_count": 155, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -4010,7 +1654,7 @@ " ...]" ] }, - "execution_count": 155, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -4021,29 +1665,7 @@ }, { "cell_type": "code", - "execution_count": 100, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "torch.Size([1, 2048, 1, 1])" - ] - }, - "execution_count": 100, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "o = torch.stack([torchvision.transforms.ToTensor()(i)])\n", - "feats = network(o)\n", - "feats.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 156, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -4071,7 +1693,7 @@ }, { "cell_type": "code", - "execution_count": 111, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -4084,47 +1706,27 @@ }, { "cell_type": "code", - "execution_count": 163, + "execution_count": 18, "metadata": {}, - "outputs": [ - { - "ename": "InvalidDimensionException", - "evalue": "Embedding dimension 3 does not match collection dimensionality 2048", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mInvalidDimensionException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[163], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mcollection\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43madd\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[43mdocuments\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtest_image\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43membeddings\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0.1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[38;5;241;43m0.2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[38;5;241;43m0.3\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mmetadatas\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43museful\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mmaybe\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m}\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[43mids\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mid1\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# must be unique, are they required?\u001b[39;49;00m\n\u001b[1;32m 6\u001b[0m \u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/api/models/Collection.py:168\u001b[0m, in \u001b[0;36mCollection.add\u001b[0;34m(self, ids, embeddings, metadatas, documents, images, uris)\u001b[0m\n\u001b[1;32m 163\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 164\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mYou must set a data loader on the collection if loading from URIs.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 165\u001b[0m )\n\u001b[1;32m 166\u001b[0m embeddings \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_embed(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_data_loader(uris))\n\u001b[0;32m--> 168\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_client\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_add\u001b[49m\u001b[43m(\u001b[49m\u001b[43mids\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mid\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43membeddings\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmetadatas\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdocuments\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43muris\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/telemetry/opentelemetry/__init__.py:143\u001b[0m, in \u001b[0;36mtrace_method..decorator..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 141\u001b[0m \u001b[38;5;28;01mglobal\u001b[39;00m tracer, granularity\n\u001b[1;32m 142\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m trace_granularity \u001b[38;5;241m<\u001b[39m granularity:\n\u001b[0;32m--> 143\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 144\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m tracer:\n\u001b[1;32m 145\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m f(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", - "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/rate_limiting/__init__.py:45\u001b[0m, in \u001b[0;36mrate_limit..decorator..wrapper\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 40\u001b[0m \u001b[38;5;129m@wraps\u001b[39m(f)\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrapper\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs: Any, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Dict[Any, Any]) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Any:\n\u001b[1;32m 42\u001b[0m \u001b[38;5;66;03m# If not rate limiting provider is present, just run and return the function.\u001b[39;00m\n\u001b[1;32m 44\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_system\u001b[38;5;241m.\u001b[39msettings\u001b[38;5;241m.\u001b[39mchroma_rate_limiting_provider_impl \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m---> 45\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 47\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m subject \u001b[38;5;129;01min\u001b[39;00m kwargs:\n\u001b[1;32m 48\u001b[0m subject_value \u001b[38;5;241m=\u001b[39m kwargs[subject]\n", - "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/api/segment.py:386\u001b[0m, in \u001b[0;36mSegmentAPI._add\u001b[0;34m(self, ids, collection_id, embeddings, metadatas, documents, uris)\u001b[0m\n\u001b[1;32m 377\u001b[0m records_to_submit \u001b[38;5;241m=\u001b[39m []\n\u001b[1;32m 378\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m r \u001b[38;5;129;01min\u001b[39;00m _records(\n\u001b[1;32m 379\u001b[0m t\u001b[38;5;241m.\u001b[39mOperation\u001b[38;5;241m.\u001b[39mADD,\n\u001b[1;32m 380\u001b[0m ids\u001b[38;5;241m=\u001b[39mids,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 384\u001b[0m uris\u001b[38;5;241m=\u001b[39muris,\n\u001b[1;32m 385\u001b[0m ):\n\u001b[0;32m--> 386\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_validate_embedding_record\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcoll\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mr\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 387\u001b[0m records_to_submit\u001b[38;5;241m.\u001b[39mappend(r)\n\u001b[1;32m 388\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_producer\u001b[38;5;241m.\u001b[39msubmit_embeddings(collection_id, records_to_submit)\n", - "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/telemetry/opentelemetry/__init__.py:143\u001b[0m, in \u001b[0;36mtrace_method..decorator..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 141\u001b[0m \u001b[38;5;28;01mglobal\u001b[39;00m tracer, granularity\n\u001b[1;32m 142\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m trace_granularity \u001b[38;5;241m<\u001b[39m granularity:\n\u001b[0;32m--> 143\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 144\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m tracer:\n\u001b[1;32m 145\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m f(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", - "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/api/segment.py:810\u001b[0m, in \u001b[0;36mSegmentAPI._validate_embedding_record\u001b[0;34m(self, collection, record)\u001b[0m\n\u001b[1;32m 808\u001b[0m add_attributes_to_current_span({\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcollection_id\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mstr\u001b[39m(collection[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mid\u001b[39m\u001b[38;5;124m\"\u001b[39m])})\n\u001b[1;32m 809\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m record[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124membedding\u001b[39m\u001b[38;5;124m\"\u001b[39m]:\n\u001b[0;32m--> 810\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_validate_dimension\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcollection\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mlen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mrecord\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43membedding\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mupdate\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/telemetry/opentelemetry/__init__.py:143\u001b[0m, in \u001b[0;36mtrace_method..decorator..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 141\u001b[0m \u001b[38;5;28;01mglobal\u001b[39;00m tracer, granularity\n\u001b[1;32m 142\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m trace_granularity \u001b[38;5;241m<\u001b[39m granularity:\n\u001b[0;32m--> 143\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 144\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m tracer:\n\u001b[1;32m 145\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m f(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", - "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/api/segment.py:825\u001b[0m, in \u001b[0;36mSegmentAPI._validate_dimension\u001b[0;34m(self, collection, dim, update)\u001b[0m\n\u001b[1;32m 823\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_collection_cache[\u001b[38;5;28mid\u001b[39m][\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mdimension\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m dim\n\u001b[1;32m 824\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m collection[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mdimension\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m!=\u001b[39m dim:\n\u001b[0;32m--> 825\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m InvalidDimensionException(\n\u001b[1;32m 826\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEmbedding dimension \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdim\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m does not match collection dimensionality \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcollection[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdimension\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 827\u001b[0m )\n\u001b[1;32m 828\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 829\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m\n", - "\u001b[0;31mInvalidDimensionException\u001b[0m: Embedding dimension 3 does not match collection dimensionality 2048" - ] - } - ], + "outputs": [], "source": [ "collection.add(\n", " documents=[\"test_image\"],\n", " embeddings=[embeddings],\n", " metadatas=[{\"useful\": \"maybe\"}],\n", - " ids=[\"id1\"] # must be unique, are they required?\n", + " ids=[\"id2\"] # must be unique, are they required?\n", ")" ] }, { "cell_type": "code", - "execution_count": 164, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'ids': ['id1'],\n", + "{'ids': ['id2'],\n", " 'embeddings': [[0.18681475520133972,\n", " 0.0,\n", " 0.39956235885620117,\n", @@ -5129,16 +2731,1129 @@ " 'metadatas': None,\n", " 'documents': None,\n", " 'uris': None,\n", - " 'data': None}" + " 'data': None,\n", + " 'included': ['embeddings']}" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "collection.get('id2',include=[\"embeddings\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": {}, + "outputs": [], + "source": [ + "index = dataset.plankton().to_dask().compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
    Dask DataFrame Structure:
    \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    Filename
    npartitions=1
    string
    ...
    \n", + "
    Dask Name: read_csv, 1 expression
    " + ], + "text/plain": [ + "Dask DataFrame Structure:\n", + " Filename\n", + "npartitions=1 \n", + " string\n", + " ...\n", + "Dask Name: read_csv, 1 expression\n", + "Expr=ReadCSV(40da6f6)" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "index\n" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "def flat_embeddings(features: torch.Tensor):\n", + " return list(features[0].squeeze(1).squeeze(1).detach().numpy().astype(float))" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": {}, + "outputs": [], + "source": [ + "def file_embeddings(row):\n", + " image_data = ImageSource(row.Filename).to_dask()\n", + " embeddings = flat_embeddings(network(prepare_image(image_data)))\n", + " collection.add(\n", + " documents=[row.Filename],\n", + " embeddings=[embeddings],\n", + " ids=[row.Filename] # must be unique, are they required?\n", + " )\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "from intake_xarray import ImageSource" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because all the images have slightly different dimensions as they come out of the FlowCam, we can't batch them\n", + "Push them through the model one by one and either build a list of `(id, [embeddings])` pairs, or potentially pop them straight into chromadb as we apply the function, which would keep it more dasklike?\n", + "\n", + "This scales ok at 8000 or so images" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "74" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "collection.count()" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 0, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/jowals/miniconda3/envs/cyto_39/lib/python3.9/site-packages/xarray/core/dataarray.py:1399: FutureWarning: None value for 'chunks' is deprecated. It will raise an error in the future. Use instead '{}'\n", + " warnings.warn(\n", + "/home/jowals/miniconda3/envs/cyto_39/lib/python3.9/site-packages/intake_xarray/image.py:474: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.\n", + " 'dims': dict(ds2.dims),\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 1, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 2, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_100.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_100.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 3, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1000.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1000.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10000.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10000.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 4, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 5, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10001.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10001.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 6, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10002.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10002.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 7, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10003.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10003.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10004.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10004.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 8, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 9, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10005.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10005.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10006.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10006.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 10, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 11, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10007.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10007.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10008.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10008.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10009.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10009.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 12, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 13, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 14, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1001.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1001.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10010.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10010.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10011.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10011.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 15, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 16, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 17, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10012.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10012.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10013.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10013.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 18, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 19, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10014.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10014.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 20, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10015.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10015.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10016.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10016.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10017.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10017.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 21, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 22, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10018.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10018.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10019.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10019.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 23, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 24, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 25, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1002.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1002.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10020.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10020.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 26, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 27, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10021.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10021.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10022.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10022.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 28, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 29, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10023.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10023.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10024.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10024.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10025.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10025.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 30, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 31, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 32, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10026.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10026.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10027.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10027.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10028.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10028.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 33, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 34, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 35, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10029.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10029.tif\n", + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1003.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_1003.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 36, dtype: object\n", + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 37, dtype: object\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Add of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10030.tif\n", + "Insert of existing embedding ID: https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untagged-images/19_10_Tank22_10030.tif\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filename https://fw-plankton-o.s3-ext.jc.rl.ac.uk/untag...\n", + "Name: 38, dtype: object\n" + ] + }, + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[85], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m res \u001b[38;5;241m=\u001b[39m \u001b[43mindex\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile_embeddings\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/pandas/core/frame.py:10374\u001b[0m, in \u001b[0;36mDataFrame.apply\u001b[0;34m(self, func, axis, raw, result_type, args, by_row, engine, engine_kwargs, **kwargs)\u001b[0m\n\u001b[1;32m 10360\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mapply\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m frame_apply\n\u001b[1;32m 10362\u001b[0m op \u001b[38;5;241m=\u001b[39m frame_apply(\n\u001b[1;32m 10363\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 10364\u001b[0m func\u001b[38;5;241m=\u001b[39mfunc,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 10372\u001b[0m kwargs\u001b[38;5;241m=\u001b[39mkwargs,\n\u001b[1;32m 10373\u001b[0m )\n\u001b[0;32m> 10374\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mop\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39m__finalize__(\u001b[38;5;28mself\u001b[39m, method\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mapply\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/pandas/core/apply.py:916\u001b[0m, in \u001b[0;36mFrameApply.apply\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 913\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mraw:\n\u001b[1;32m 914\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapply_raw(engine\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mengine, engine_kwargs\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mengine_kwargs)\n\u001b[0;32m--> 916\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply_standard\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/pandas/core/apply.py:1063\u001b[0m, in \u001b[0;36mFrameApply.apply_standard\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1061\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mapply_standard\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 1062\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mengine \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpython\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m-> 1063\u001b[0m results, res_index \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply_series_generator\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1064\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 1065\u001b[0m results, res_index \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapply_series_numba()\n", + "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/pandas/core/apply.py:1081\u001b[0m, in \u001b[0;36mFrameApply.apply_series_generator\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1078\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m option_context(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmode.chained_assignment\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 1079\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i, v \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(series_gen):\n\u001b[1;32m 1080\u001b[0m \u001b[38;5;66;03m# ignore SettingWithCopy here in case the user mutates\u001b[39;00m\n\u001b[0;32m-> 1081\u001b[0m results[i] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mv\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1082\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(results[i], ABCSeries):\n\u001b[1;32m 1083\u001b[0m \u001b[38;5;66;03m# If we have a view on v, we need to make a copy because\u001b[39;00m\n\u001b[1;32m 1084\u001b[0m \u001b[38;5;66;03m# series_generator will swap out the underlying data\u001b[39;00m\n\u001b[1;32m 1085\u001b[0m results[i] \u001b[38;5;241m=\u001b[39m results[i]\u001b[38;5;241m.\u001b[39mcopy(deep\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m)\n", + "Cell \u001b[0;32mIn[84], line 6\u001b[0m, in \u001b[0;36mfile_embeddings\u001b[0;34m(row)\u001b[0m\n\u001b[1;32m 4\u001b[0m image_data \u001b[38;5;241m=\u001b[39m ImageSource(row\u001b[38;5;241m.\u001b[39mFilename)\u001b[38;5;241m.\u001b[39mto_dask()\n\u001b[1;32m 5\u001b[0m embeddings \u001b[38;5;241m=\u001b[39m flat_embeddings(network(prepare_image(image_data)))\n\u001b[0;32m----> 6\u001b[0m \u001b[43mcollection\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43madd\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 7\u001b[0m \u001b[43m \u001b[49m\u001b[43mdocuments\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43mrow\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mFilename\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 8\u001b[0m \u001b[43m \u001b[49m\u001b[43membeddings\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43membeddings\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m#metadatas=[{\"useful\": \"maybe\"}],\u001b[39;49;00m\n\u001b[1;32m 10\u001b[0m \u001b[43m \u001b[49m\u001b[43mids\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43mrow\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mFilename\u001b[49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# must be unique, are they required?\u001b[39;49;00m\n\u001b[1;32m 11\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/api/models/Collection.py:80\u001b[0m, in \u001b[0;36mCollection.add\u001b[0;34m(self, ids, embeddings, metadatas, documents, images, uris)\u001b[0m\n\u001b[1;32m 40\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21madd\u001b[39m(\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 42\u001b[0m ids: OneOrMany[ID],\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 52\u001b[0m uris: Optional[OneOrMany[URI]] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 53\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 54\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Add embeddings to the data store.\u001b[39;00m\n\u001b[1;32m 55\u001b[0m \u001b[38;5;124;03m Args:\u001b[39;00m\n\u001b[1;32m 56\u001b[0m \u001b[38;5;124;03m ids: The ids of the embeddings you wish to add\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 72\u001b[0m \n\u001b[1;32m 73\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m 74\u001b[0m (\n\u001b[1;32m 75\u001b[0m ids,\n\u001b[1;32m 76\u001b[0m embeddings,\n\u001b[1;32m 77\u001b[0m metadatas,\n\u001b[1;32m 78\u001b[0m documents,\n\u001b[1;32m 79\u001b[0m uris,\n\u001b[0;32m---> 80\u001b[0m ) \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_validate_and_prepare_embedding_set\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 81\u001b[0m \u001b[43m \u001b[49m\u001b[43mids\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43membeddings\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmetadatas\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdocuments\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mimages\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43muris\u001b[49m\n\u001b[1;32m 82\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 84\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_client\u001b[38;5;241m.\u001b[39m_add(ids, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mid, embeddings, metadatas, documents, uris)\n", + "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/api/models/CollectionCommon.py:261\u001b[0m, in \u001b[0;36mCollectionCommon._validate_and_prepare_embedding_set\u001b[0;34m(self, ids, embeddings, metadatas, documents, images, uris)\u001b[0m\n\u001b[1;32m 234\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_validate_and_prepare_embedding_set\u001b[39m(\n\u001b[1;32m 235\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 236\u001b[0m ids: OneOrMany[ID],\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 252\u001b[0m Optional[URIs],\n\u001b[1;32m 253\u001b[0m ]:\n\u001b[1;32m 254\u001b[0m (\n\u001b[1;32m 255\u001b[0m ids,\n\u001b[1;32m 256\u001b[0m embeddings,\n\u001b[1;32m 257\u001b[0m metadatas,\n\u001b[1;32m 258\u001b[0m documents,\n\u001b[1;32m 259\u001b[0m images,\n\u001b[1;32m 260\u001b[0m uris,\n\u001b[0;32m--> 261\u001b[0m ) \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_validate_embedding_set\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 262\u001b[0m \u001b[43m \u001b[49m\u001b[43mids\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43membeddings\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmetadatas\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdocuments\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mimages\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43muris\u001b[49m\n\u001b[1;32m 263\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 265\u001b[0m \u001b[38;5;66;03m# We need to compute the embeddings if they're not provided\u001b[39;00m\n\u001b[1;32m 266\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m embeddings \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 267\u001b[0m \u001b[38;5;66;03m# At this point, we know that one of documents or images are provided from the validation above\u001b[39;00m\n", + "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/api/models/CollectionCommon.py:165\u001b[0m, in \u001b[0;36mCollectionCommon._validate_embedding_set\u001b[0;34m(self, ids, embeddings, metadatas, documents, images, uris, require_embeddings_or_data)\u001b[0m\n\u001b[1;32m 141\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_validate_embedding_set\u001b[39m(\n\u001b[1;32m 142\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 143\u001b[0m ids: OneOrMany[ID],\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 161\u001b[0m Optional[URIs],\n\u001b[1;32m 162\u001b[0m ]:\n\u001b[1;32m 163\u001b[0m valid_ids \u001b[38;5;241m=\u001b[39m validate_ids(maybe_cast_one_to_many_ids(ids))\n\u001b[1;32m 164\u001b[0m valid_embeddings \u001b[38;5;241m=\u001b[39m (\n\u001b[0;32m--> 165\u001b[0m \u001b[43mvalidate_embeddings\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 166\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_normalize_embeddings\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmaybe_cast_one_to_many_embedding\u001b[49m\u001b[43m(\u001b[49m\u001b[43membeddings\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 167\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 168\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m embeddings \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 169\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 170\u001b[0m )\n\u001b[1;32m 171\u001b[0m valid_metadatas \u001b[38;5;241m=\u001b[39m (\n\u001b[1;32m 172\u001b[0m validate_metadatas(maybe_cast_one_to_many_metadata(metadatas))\n\u001b[1;32m 173\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m metadatas \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 174\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 175\u001b[0m )\n\u001b[1;32m 176\u001b[0m valid_documents \u001b[38;5;241m=\u001b[39m (\n\u001b[1;32m 177\u001b[0m maybe_cast_one_to_many_document(documents)\n\u001b[1;32m 178\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m documents \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 179\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 180\u001b[0m )\n", + "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/api/types.py:502\u001b[0m, in \u001b[0;36mvalidate_embeddings\u001b[0;34m(embeddings)\u001b[0m\n\u001b[1;32m 497\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(embedding) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 498\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 499\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mExpected each embedding in the embeddings to be a non-empty list, got empty embedding at pos \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mi\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 500\u001b[0m )\n\u001b[1;32m 501\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mall\u001b[39m(\n\u001b[0;32m--> 502\u001b[0m [\n\u001b[1;32m 503\u001b[0m \u001b[38;5;28misinstance\u001b[39m(value, (\u001b[38;5;28mint\u001b[39m, \u001b[38;5;28mfloat\u001b[39m)) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(value, \u001b[38;5;28mbool\u001b[39m)\n\u001b[1;32m 504\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m value \u001b[38;5;129;01min\u001b[39;00m embedding\n\u001b[1;32m 505\u001b[0m ]\n\u001b[1;32m 506\u001b[0m ):\n\u001b[1;32m 507\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 508\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mExpected each value in the embedding to be a int or float, got an embedding with \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 509\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlist\u001b[39m(\u001b[38;5;28mset\u001b[39m([\u001b[38;5;28mtype\u001b[39m(value)\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mfor\u001b[39;00m\u001b[38;5;250m \u001b[39mvalue\u001b[38;5;250m \u001b[39m\u001b[38;5;129;01min\u001b[39;00m\u001b[38;5;250m \u001b[39membedding]))\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m - \u001b[39m\u001b[38;5;132;01m{\u001b[39;00membedding\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 510\u001b[0m )\n\u001b[1;32m 511\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m embeddings\n", + "File \u001b[0;32m~/miniconda3/envs/cyto_39/lib/python3.9/site-packages/chromadb/api/types.py:503\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 497\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(embedding) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 498\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 499\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mExpected each embedding in the embeddings to be a non-empty list, got empty embedding at pos \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mi\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 500\u001b[0m )\n\u001b[1;32m 501\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mall\u001b[39m(\n\u001b[1;32m 502\u001b[0m [\n\u001b[0;32m--> 503\u001b[0m \u001b[38;5;28;43misinstance\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mint\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mfloat\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(value, \u001b[38;5;28mbool\u001b[39m)\n\u001b[1;32m 504\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m value \u001b[38;5;129;01min\u001b[39;00m embedding\n\u001b[1;32m 505\u001b[0m ]\n\u001b[1;32m 506\u001b[0m ):\n\u001b[1;32m 507\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 508\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mExpected each value in the embedding to be a int or float, got an embedding with \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 509\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlist\u001b[39m(\u001b[38;5;28mset\u001b[39m([\u001b[38;5;28mtype\u001b[39m(value)\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mfor\u001b[39;00m\u001b[38;5;250m \u001b[39mvalue\u001b[38;5;250m \u001b[39m\u001b[38;5;129;01min\u001b[39;00m\u001b[38;5;250m \u001b[39membedding]))\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m - \u001b[39m\u001b[38;5;132;01m{\u001b[39;00membedding\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 510\u001b[0m )\n\u001b[1;32m 511\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m embeddings\n", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "res = index.apply(file_embeddings, axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "74" + ] + }, + "execution_count": 87, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "collection.count()" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/jowals/miniconda3/envs/cyto_39/lib/python3.9/site-packages/xarray/core/dataarray.py:1399: FutureWarning: None value for 'chunks' is deprecated. It will raise an error in the future. Use instead '{}'\n", + " warnings.warn(\n", + "/home/jowals/miniconda3/envs/cyto_39/lib/python3.9/site-packages/intake_xarray/image.py:474: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.\n", + " 'dims': dict(ds2.dims),\n" + ] + }, + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.DataArray (y: 24, x: 15, channel: 3)> Size: 1kB\n",
    +       "dask.array<xarray-<this-array>, shape=(24, 15, 3), dtype=uint8, chunksize=(24, 15, 3), chunktype=numpy.ndarray>\n",
    +       "Coordinates:\n",
    +       "  * y        (y) int64 192B 0 1 2 3 4 5 6 7 8 9 ... 15 16 17 18 19 20 21 22 23\n",
    +       "  * x        (x) int64 120B 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14\n",
    +       "  * channel  (channel) int64 24B 0 1 2
    " + ], + "text/plain": [ + " Size: 1kB\n", + "dask.array, shape=(24, 15, 3), dtype=uint8, chunksize=(24, 15, 3), chunktype=numpy.ndarray>\n", + "Coordinates:\n", + " * y (y) int64 192B 0 1 2 3 4 5 6 7 8 9 ... 15 16 17 18 19 20 21 22 23\n", + " * x (x) int64 120B 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14\n", + " * channel (channel) int64 24B 0 1 2" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "i = ImageSource(index.loc[0].Filename).to_dask()\n", + "i" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/jowals/miniconda3/envs/cyto_39/lib/python3.9/site-packages/xarray/core/dataarray.py:1399: FutureWarning: None value for 'chunks' is deprecated. It will raise an error in the future. Use instead '{}'\n", + " warnings.warn(\n", + "/home/jowals/miniconda3/envs/cyto_39/lib/python3.9/site-packages/intake_xarray/image.py:474: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.\n", + " 'dims': dict(ds2.dims),\n" + ] + }, + { + "data": { + "text/plain": [ + "tensor([[[[0.1368]],\n", + "\n", + " [[0.1237]],\n", + "\n", + " [[0.0324]],\n", + "\n", + " ...,\n", + "\n", + " [[0.0000]],\n", + "\n", + " [[0.3849]],\n", + "\n", + " [[0.0000]]]], grad_fn=)" ] }, - "execution_count": 164, + "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "collection.get('id1',include=[\"embeddings\"])" + "network(prepare_image(i))" ] } ], From e5530bfee733bba00b052979ae84a8438cae7bbc Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 17:15:08 +0100 Subject: [PATCH 15/16] clear outputs from the embedding notebook, add the proof of concept --- environment.yml | 2 + notebooks/VectorSearch.ipynb | 138 +++++++++++++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/environment.yml b/environment.yml index 9243240..a23b288 100644 --- a/environment.yml +++ b/environment.yml @@ -10,10 +10,12 @@ dependencies: - dask - pip: - pytest + - imagecodecs - intake # for reading scivision - torch==1.10.0 # install before cefas_scivision; it needs this version - scivision - scikit-image - setuptools==69.5.1 # because this bug https://github.com/pytorch/serve/issues/3176 + - tiffile - git+https://github.com/alan-turing-institute/plankton-cefas-scivision@main # torch version - chromadb diff --git a/notebooks/VectorSearch.ipynb b/notebooks/VectorSearch.ipynb index d8c5b3e..e1543f6 100644 --- a/notebooks/VectorSearch.ipynb +++ b/notebooks/VectorSearch.ipynb @@ -2,13 +2,17 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "import sys\n", "sys.path.append('../')\n", - "from cyto_ml.data.vectorstore import vector_store" + "import random\n", + "from cyto_ml.data.vectorstore import vector_store\n", + "from skimage import io\n", + "from matplotlib import pyplot as plt\n", + "from mpl_toolkits.axes_grid1 import ImageGrid" ] }, { @@ -31,17 +35,141 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:root:Collection plankton already exists\n" + ] + }, + { + "data": { + "text/plain": [ + "8805" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "store = vector_store('plankton')\n", + "store.count()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "See what we can get out of the box with Chroma \n", + "https://github.com/neo-con/chromadb-tutorial/tree/main/4.%20Querying%20a%20Collection/1.%20Querying%20Embeddings\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Without parameters, this gives us back all the document identifiers (if you want the document - in our case the same URL as the ID - or the embeddings, you have to ask `get` for that with an `include=['field','names']`)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "res = store.get()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "test_image_url = random.choice(res['ids'])\n", + "test_embed = store.get([test_image_url],include=['embeddings'])['embeddings']\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Query for the 24 closest image matches (plus the image itself which comes back with a distance of 0)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, "metadata": {}, "outputs": [], "source": [ - "store = vector_store()" + "results = store.query(\n", + " query_embeddings=test_embed,\n", + " n_results=25\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that here we're looking at `results['ids'][0]` because `chromadb` will always assume we queried for a list.\n", + "Plot the closest matches labelled with their distance from the original." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure(figsize=(10., 10.))\n", + "grid = ImageGrid(fig, 111, # similar to subplot(111)\n", + " nrows_ncols=(5, 5), # creates 2x2 grid of axes\n", + " axes_pad=0.2, # pad between axes in inch.\n", + " )\n", + "\n", + "for index, ax in enumerate(grid):\n", + " # Iterating over the grid returns the Axes.\n", + " ax.imshow(io.imread(results['ids'][0][index]))\n", + " dist = results['distances'][0][index]\n", + " ax.set_title(f'{dist:.2f}', fontsize=8)" ] } ], "metadata": { + "kernelspec": { + "display_name": "cyto_39", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.19" } }, "nbformat": 4, From b2d2aa1e122a00a31ab163d31a77512aca142642 Mon Sep 17 00:00:00 2001 From: Jo Walsh Date: Mon, 1 Jul 2024 17:45:08 +0100 Subject: [PATCH 16/16] update the test action to reflect they've moved location --- .github/workflows/pytest_coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest_coverage.yml b/.github/workflows/pytest_coverage.yml index f99692d..559f1b7 100644 --- a/.github/workflows/pytest_coverage.yml +++ b/.github/workflows/pytest_coverage.yml @@ -24,7 +24,7 @@ jobs: python-version: ${{ matrix.python-version }} auto-activate-base: false - run: pip install pytest-cov - - run: python -m pytest --cov=cyto_ml --cov-report xml:coverage.xml tests/ + - run: python -m pytest --cov=cyto_ml --cov-report xml:coverage.xml - uses: actions/upload-artifact@v4 with: name: coverage.xml