Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5a22b00
Initial IQR integration
floryst Nov 5, 2024
68f2497
IQR refinement loop
floryst Nov 14, 2024
54580a8
Show IQR button when selecting a card
floryst Nov 19, 2024
f0f2828
Reformatting and load IQR images
floryst Nov 19, 2024
0adcbb2
Rename migration script
floryst Nov 19, 2024
4454083
Bring in styling changes from main
floryst Nov 20, 2024
9dfd9b2
Improve image selection; click to focus site
floryst Nov 20, 2024
5dc059e
IQR: simplify docker-compose
floryst Nov 21, 2024
78c93a0
IQR: add usage docs
floryst Nov 21, 2024
770105a
IQR: guard against None and return empty images
floryst Nov 21, 2024
c330b03
IQR: fetch queried site's image
floryst Nov 22, 2024
e59e1ef
IQR: show satellite images of matching sites
floryst Nov 22, 2024
6076a9b
Zoom the map faster
floryst Nov 25, 2024
8c5618d
IQR: improve sidebar UI to match mockups
floryst Nov 25, 2024
4a2be66
IQR: Show loading indicator when refining
floryst Nov 25, 2024
df48b24
IQR: rename button to "Refine Query"
floryst Nov 25, 2024
c954876
IQR: site vector tiles for IQR results
floryst Nov 25, 2024
73ae883
IQR: make images clickable
floryst Nov 25, 2024
2c89e08
IQR: show image viewer when clicking on a site
floryst Nov 25, 2024
ed38b79
IQR: closeable sidebar
floryst Nov 25, 2024
6d43d93
IQR: filter only WV images
floryst Nov 25, 2024
11724db
IQR: resize map when container changes
floryst Nov 25, 2024
790ff00
IQR: show loading in button
floryst Nov 25, 2024
aa9eb6f
IQR: show IQR layers only if IQR state exists
floryst Nov 25, 2024
3fcb6ca
IQR: show clusters when zoomed out
floryst Nov 26, 2024
ee61207
IQR: limit the height of site images
floryst Nov 26, 2024
e21bd1d
IQR: toggle satellite image with image browser
floryst Nov 26, 2024
d8addf6
IQR: clear IQR session when closing the panel
floryst Nov 26, 2024
84f53fa
IQR: support custom fit bounds options
floryst Nov 26, 2024
b6b6d18
IQR: fix returning fewer than MAX_RESULTS results
floryst Nov 26, 2024
f136a69
IQR: format files
floryst Nov 26, 2024
544f700
Merge remote-tracking branch 'origin/main' into iqr-integration
floryst Nov 26, 2024
5e85d66
IQR: better fit bounds params
floryst Nov 26, 2024
743cfb7
IQR: fix conditional
floryst Nov 26, 2024
b483756
IQR: format ts files
floryst Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docker-compose.override.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,72 @@ services:
# Run the npm development script
npm run dev
"

iqr_rest:
image: rdwatch-smqtk-iqr
build:
dockerfile: docker/smqtk-iqr.Dockerfile
context: .
# needed for dependencies to properly install
platform: linux/amd64
command:
["runApplication", "-a", "IqrService", "-c", "runApp.IqrRestService.json"]
ports:
- 5001:5001
working_dir: /iqr
volumes:
# where rest + search app configs are stored
- ./iqr:/iqr

# needed for accessing the workdir from the config
# EDIT THIS VOLUME MOUNT BEFORE RUNNING
#- /path/to/iqr-data/workdir:/iqr/workdir
- /Users/forrest.li/scratch/iqr/workdir:/iqr/workdir

# needed to make sure faiss_index exists, otherwise IQR initialization won't work
# EDIT THIS VOLUME MOUNT BEFORE RUNNING
#- /path/to/iqr-data/models:/iqr/models
- /Users/forrest.li/scratch/iqr/models:/iqr/models

# only needed for the built-in IQR interface
#smqtk_mongo:
# image: mongo:latest
# ports:
# - 27017:27017
# volumes:
# - smqtk-mongo:/data/db
#iqr_web:
# image: rdwatch-smqtk-iqr
# # needed for dependencies to properly install
# platform: linux/amd64
# command:
# [
# "runApplication",
# "-a",
# "IqrSearchDispatcher",
# "-c",
# "runApp.IqrSearchApp.json",
# ]
# ports:
# - 5000:5000
# working_dir: /iqr
# volumes:
# # where rest + search app configs are stored
# # - ./iqr/workdir:/iqr/workdir
# - ./iqr:/iqr

# # needed for accessing the workdir from the config
# - /SMQTK-IQR/docs/tutorials/tutorial_003_geowatch_descriptors/workdir:/iqr/workdir

# # needed to make sure faiss_index exists, otherwise IQR initialization won't work
# - /SMQTK-IQR/docs/tutorials/tutorial_003_geowatch_descriptors/models:/iqr/models

# # The workdir/processed/chips/manifest.json file stores absolute paths, so we add another bind mount to satisfy the abs paths
# # from when I generated the outputs.
# - /SMQTK-IQR/docs/tutorials/tutorial_003_geowatch_descriptors/workdir:/SMQTK-IQR/docs/tutorials/tutorial_003_geowatch_descriptors/workdir
# depends_on:
# - smqtk_mongo
# - iqr_rest

volumes:
celery-SAM-model:
32 changes: 32 additions & 0 deletions docker/smqtk-iqr.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM python:3.11

WORKDIR /
RUN apt update && apt install -y git gdal-bin libgdal-dev
RUN git clone https://github.com/Erotemic/SMQTK-IQR.git
RUN git clone https://github.com/Kitware/SMQTK-Descriptors.git

WORKDIR /SMQTK-IQR
RUN git checkout dev/add-tutorial-3
RUN pip install -e .
RUN pip install faiss-cpu==1.8.0 \
"psycopg2-binary>=2.9.5,<3.0.0" \
scriptconfig \
ubelt \
rich \
kwcoco \
opencv-python-headless \
girder-client \
# this version matches the one from python:3.11 apt install gdal-bin
gdal==3.6.2 \
geowatch \
kwcoco \
kwgis \
kwutil \
scriptconfig \
# extra pkg for running `geowatch torch_model_stats ...`
netharn

WORKDIR /SMQTK-Descriptors
RUN pip install -e .

WORKDIR /SMQTK-IQR
68 changes: 68 additions & 0 deletions docs/IQR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Iterative Query Refinement

## Getting Started Locally

### Initial IQR Data

To avoid generating the IQR mappings from scratch, you should have a `iqr-data.tar.gz` file with the following contents:

```
models/
faiss_index_params.json
faiss_index
sites/
*.geojson
workdir/
data.memorySet.pickle
descriptor_set.pickle
idx2uid.mem_kvstore.pickle
uid2idx.mem_kvstore.pickle
```

Extract `iqr-data.tar.gz` to a suitable location. We will refer to it as `/path/to/iqr-data` from here on out.

### Docker Compose Volumes

Edit the `iqr_rest` service in `docker-compose.override.yaml`. Specifically, the volumes must be updated with the following mounts. These are commented accordingly in the `docker-compose.override.yaml` file.

**IMPORTANT**: Replace the `/path/to/iqr-data` path prefix with the correct path.

- `/path/to/iqr-data/workdir:/iqr/workdir`
- `/path/to/iqr-data/models:/iqr/models`

### Ingesting The Sites

First, start the docker services and perform the requisite migrations and setup.

```bash
docker compose up -d
docker compose run --rm django poetry run django-admin migrate
docker compose run --rm django poetry run django-admin createsuperuser
docker compose run --rm django poetry run django-admin loaddata lookups
```

Now, we can ingest the sites provided in the IQR data archive. The following snippet assumes a bash shell currently located in the RD-WATCH repo root.

**IMPORTANT**: Replace the `/path/to/iqr-data` path prefix with the correct path.

```bash
for region in "KR_R001" "KR_R002" "CH_R001" "NZ_R001"
do
python ./scripts/loadModelRun.py "$region" "/path/to/iqr-data/sites/${region}_*.geojson" --title "$region" --performer_shortcode TE
done
```

### Loading The WorldView Images

To ensure that the IQR query results have an associated image, open the RD-WATCH interface in the browser and download the "WV" satellite chips for every model run. This may take a long time!

## Running IQR through RD-WATCH

1. Navigate to <http://localhost:8080/#/iqr> to enable IQR.
1. Select a model run, and then select a site. If the site has IQR enabled, then there will be an IQR button (as shown below). Clicking this button will initiate an IQR query on that site, and a right sidebar will show up with the results.

<img width="32" height="32" src="images/iqr-button.png">

1. IQR refinement occurs in two steps:
1. Update positive, neutral, and negative results in the IQR result listing.
1. Run "Refine Query" to generate a new list of IQR results.
Binary file added docs/images/iqr-button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
148 changes: 148 additions & 0 deletions iqr/runApp.IqrRestService.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
{
"flask_app": {
"BASIC_AUTH_PASSWORD": "demo",
"BASIC_AUTH_USERNAME": "demo",
"SECRET_KEY": "MySuperUltraSecret",
"debug_server": true
},
"server": {
"host": "0.0.0.0",
"port": 5001
},
"iqr_service": {
"plugin_notes": {
"classification_factory": "Selection of the backend in which classifications are stored. The in-memory version is recommended because normal caching mechanisms will not account for the variety of classifiers that can potentially be created via this utility.",
"classifier_config": "The configuration to use for training and using classifiers for the /classifier endpoint. When configuring a classifier for use, don't fill out model persistence values as many classifiers may be created and thrown away during this service's operation.",
"descriptor_factory": "What descriptor element factory to use when asked to compute a descriptor on data.",
"descriptor_generator": "Descriptor generation algorithm to use when requested to describe data.",
"descriptor_set": "This is the index from which given positive and negative example descriptors are retrieved from. Not used for nearest neighbor querying. This index must contain all descriptors that could possibly be used as positive/negative examples and updated accordingly.",
"neighbor_index": "This is the neighbor index to pull initial near-positive descriptors from.",
"relevancy_index_config": "The relevancy index config provided should not have persistent storage configured as it will be used in such a way that instances are created, built and destroyed often."
},
"plugins": {
"classification_factory": {
"smqtk_classifier.impls.classification_element.memory.MemoryClassificationElement": {},
"type": "smqtk_classifier.impls.classification_element.memory.MemoryClassificationElement"
},
"classifier_config": {
"smqtk_classifier.impls.classify_descriptor_supervised.sklearn_logistic_regression.SkLearnLogisticRegression": {
},
"type": "smqtk_classifier.impls.classify_descriptor_supervised.sklearn_logistic_regression.SkLearnLogisticRegression"
},
"descriptor_factory": {
"smqtk_descriptors.impls.descriptor_element.memory.DescriptorMemoryElement": {},
"type": "smqtk_descriptors.impls.descriptor_element.memory.DescriptorMemoryElement"
},
"descriptor_generator": {
"smqtk_descriptors.impls.descriptor_generator.prepopulated.PrePopulatedDescriptorGenerator": {
},
"type": "smqtk_descriptors.impls.descriptor_generator.prepopulated.PrePopulatedDescriptorGenerator"
},
"descriptor_set": {
"smqtk_descriptors.impls.descriptor_set.memory.MemoryDescriptorSet": {
"cache_element": {
"smqtk_dataprovider.impls.data_element.file.DataFileElement": {
"explicit_mimetype": null,
"filepath": "workdir/descriptor_set.pickle",
"readonly": false
},
"type": "smqtk_dataprovider.impls.data_element.file.DataFileElement"
},
"pickle_protocol": -1
},
"type": "smqtk_descriptors.impls.descriptor_set.memory.MemoryDescriptorSet"
},
"neighbor_index": {
"smqtk_indexing.impls.nn_index.faiss.FaissNearestNeighborsIndex": {
"descriptor_set": {
"smqtk_descriptors.impls.descriptor_set.memory.MemoryDescriptorSet": {
"cache_element": {
"smqtk_dataprovider.impls.data_element.file.DataFileElement": {
"explicit_mimetype": null,
"filepath": "workdir/descriptor_set.pickle",
"readonly": false
},
"type": "smqtk_dataprovider.impls.data_element.file.DataFileElement"
},
"pickle_protocol": -1
},
"type": "smqtk_descriptors.impls.descriptor_set.memory.MemoryDescriptorSet"
},
"factory_string": "IDMap,Flat",
"gpu_id": 0,
"idx2uid_kvs": {
"smqtk_dataprovider.impls.key_value_store.memory.MemoryKeyValueStore": {
"cache_element": {
"smqtk_dataprovider.impls.data_element.file.DataFileElement": {
"explicit_mimetype": null,
"filepath": "workdir/idx2uid.mem_kvstore.pickle",
"readonly": false
},
"type": "smqtk_dataprovider.impls.data_element.file.DataFileElement"
}
},
"type": "smqtk_dataprovider.impls.key_value_store.memory.MemoryKeyValueStore"
},
"uid2idx_kvs": {
"smqtk_dataprovider.impls.key_value_store.memory.MemoryKeyValueStore": {
"cache_element": {
"smqtk_dataprovider.impls.data_element.file.DataFileElement": {
"explicit_mimetype": null,
"filepath": "workdir/uid2idx.mem_kvstore.pickle",
"readonly": false
},
"type": "smqtk_dataprovider.impls.data_element.file.DataFileElement"
}
},
"type": "smqtk_dataprovider.impls.key_value_store.memory.MemoryKeyValueStore"
},
"index_element": {
"smqtk_dataprovider.impls.data_element.file.DataFileElement": {
"filepath": "models/faiss_index",
"readonly": false
},
"type": "smqtk_dataprovider.impls.data_element.file.DataFileElement"
},
"index_param_element": {
"smqtk_dataprovider.impls.data_element.file.DataFileElement": {
"filepath": "models/faiss_index_params.json",
"readonly": false
},
"type": "smqtk_dataprovider.impls.data_element.file.DataFileElement"
},
"ivf_nprobe": 64,
"metric_type": "l2",
"random_seed": 0,
"read_only": false,
"use_gpu": false
},
"type": "smqtk_indexing.impls.nn_index.faiss.FaissNearestNeighborsIndex"
},
"rank_relevancy_with_feedback": {
"smqtk_relevancy.impls.rank_relevancy.margin_sampling.RankRelevancyWithMarginSampledFeedback": {
"rank_relevancy": {
"smqtk_relevancy.impls.rank_relevancy.wrap_classifier.RankRelevancyWithSupervisedClassifier": {
"classifier_inst": {
"smqtk_classifier.impls.classify_descriptor_supervised.sklearn_logistic_regression.SkLearnLogisticRegression": {
},
"type": "smqtk_classifier.impls.classify_descriptor_supervised.sklearn_logistic_regression.SkLearnLogisticRegression"
}
},
"type": "smqtk_relevancy.impls.rank_relevancy.wrap_classifier.RankRelevancyWithSupervisedClassifier"
},
"n": 10,
"center": 0.5
},
"type": "smqtk_relevancy.impls.rank_relevancy.margin_sampling.RankRelevancyWithMarginSampledFeedback"
}
},
"session_control": {
"positive_seed_neighbors": 500,
"session_expiration": {
"check_interval_seconds": 30,
"enabled": true,
"session_timeout": 3600
}
}
}
}
36 changes: 36 additions & 0 deletions iqr/runApp.IqrSearchApp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"flask_app": {
"BASIC_AUTH_PASSWORD": "demo",
"BASIC_AUTH_USERNAME": "demo",
"SECRET_KEY": "MySuperUltraSecret",
"debug": true
},
"iqr_tabs": {
"GEOWATCH_DEMO": {
"data_set": {
"smqtk_dataprovider.impls.data_set.memory.DataMemorySet": {
"cache_element": {
"smqtk_dataprovider.impls.data_element.file.DataFileElement": {
"explicit_mimetype": null,
"filepath": "workdir/data.memorySet.cache",
"readonly": false
},
"type": "smqtk_dataprovider.impls.data_element.file.DataFileElement"
},
"pickle_protocol": -1
},
"type": "smqtk_dataprovider.impls.data_set.memory.DataMemorySet"
},
"iqr_service_url": "iqr_rest:5001",
"working_directory": "workdir"
}
},
"mongo": {
"database": "smqtk",
"server": "smqtk_mongo:27017"
},
"server": {
"host": "0.0.0.0",
"port": 5000
}
}
2 changes: 2 additions & 0 deletions rdwatch/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .views import site
from .views.animation import router as animation_router
from .views.iqr import router as iqr_router
from .views.model_run import router as model_run_router
from .views.performer import router as performer_router
from .views.region import router as region_router
Expand All @@ -27,6 +28,7 @@
api.add_router('/sites/', site.router)
api.add_router('/satellite-fetching/', satellite_fetching_router)
api.add_router('/animation/', animation_router)
api.add_router('/iqr/', iqr_router)


# useful for getting information back about validation errors
Expand Down
19 changes: 19 additions & 0 deletions rdwatch/core/migrations/0042_siteevaluation_smqtk_uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.0.9 on 2024-10-25 11:20

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
('core', '0041_merge_20241021_1458'),
]

operations = [
migrations.AddField(
model_name='siteevaluation',
name='smqtk_uuid',
field=models.CharField(
blank=True, help_text='SMQTK UUID', max_length=256, null=True
),
),
]
Loading