Skip to content

Commit

Permalink
Merge pull request #63 from databricks-industry-solutions/feature/mon…
Browse files Browse the repository at this point in the history
…ai_integration

MONAILabel Integration
  • Loading branch information
erinaldidb authored Oct 23, 2024
2 parents 759b13c + 5277e1d commit 1b7c92b
Show file tree
Hide file tree
Showing 182 changed files with 476,209 additions and 1,578 deletions.
48 changes: 48 additions & 0 deletions 05-MONAILabel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Databricks notebook source
# MAGIC %md
# MAGIC # Introduction
# MAGIC
# MAGIC This notebook is designed to install the MONAILabel_Pixels and databricks-sdk libraries. It provides the necessary instructions to install these libraries using the `%pip` command and restart the Python library using `dbutils.library.restartPython()`.
# MAGIC
# MAGIC # MONAILabel
# MAGIC
# MAGIC MONAILabel is a framework for creating interactive and customizable annotation workflows for medical imaging data. It provides a user-friendly interface for annotating medical images and supports various annotation tasks such as segmentation, classification, etc.
# MAGIC
# MAGIC # Integration with OHIF Viewer:
# MAGIC
# MAGIC MONAILabel can be integrated with the OHIF Viewer to provide a seamless annotation experience. The OHIF Viewer is a web-based medical image viewer that allows users to view, annotate, and analyze medical images. By integrating MONAILabel with the OHIF Viewer, users can leverage the advanced annotation capabilities of MONAILabel directly within the viewer interface. This integration enables efficient and accurate annotation of medical images, enhancing the overall workflow for medical image analysis and research.

# COMMAND ----------

# DBTITLE 1,Install MONAILabel_Pixels and databricks-sdk
# MAGIC %pip install git+https://github.com/erinaldidb/MONAILabel_Pixels.git databricks-sdk --upgrade

# COMMAND ----------

dbutils.library.restartPython()

# COMMAND ----------

# MAGIC %run ./config/proxy_prep

# COMMAND ----------

init_widgets()

# COMMAND ----------

# DBTITLE 1,MONAILabel Server Address Generation in Databricks
init_env()
displayHTML(f"<h1>Use the following link as MONAILabel server address</h1><br><h2>{get_proxy_url()}")

# COMMAND ----------

# DBTITLE 1,Downloading Radiology Apps with MonaiLabel
# MAGIC %sh
# MAGIC monailabel apps --download --name radiology --output /local_disk0/monai/apps/

# COMMAND ----------

# DBTITLE 1,Monailabel Radiology Segmentation
# MAGIC %sh
# MAGIC monailabel start_server --app /local_disk0/monai/apps/radiology --studies $DATABRICKS_HOST --conf models segmentation --table $DATABRICKS_PIXELS_TABLE
75 changes: 40 additions & 35 deletions OHIF_Viewer.py → 06-OHIF-Viewer.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
# Databricks notebook source
# MAGIC %pip install dbtunnel[fastapi] httpx databricks-sdk --upgrade
# MAGIC %pip install fastapi uvicorn httpx databricks-sdk --upgrade

# COMMAND ----------

dbutils.library.restartPython()

# COMMAND ----------

from databricks.sdk import WorkspaceClient

w = WorkspaceClient()

dbutils.widgets.text("table", "main.pixels_solacc.object_catalog", label="1.0 Catalog Schema Table to store object metadata into")
dbutils.widgets.text("sqlWarehouseID", "", label="2.0 SQL Warehouse")

sql_warehouse_id = dbutils.widgets.get("sqlWarehouseID")
table = dbutils.widgets.get("table")

if not spark.catalog.tableExists(table):
raise Exception("The configured table does not exist!")

if sql_warehouse_id == "":
raise Exception("SQL Warehouse ID is mandatory!")
else:
wh = w.warehouses.get(id=sql_warehouse_id)
print(f"Using '{wh.as_dict()['name']}' as SQL Warehouse")
# MAGIC %run ./config/proxy_prep

# COMMAND ----------

Expand All @@ -38,20 +21,25 @@
from pathlib import Path
import dbx.pixels.resources

init_widgets()
init_env()

workspace_id = get_context().workspaceId
cluster_id = get_context().clusterId

port = 3000

path = Path(dbx.pixels.__file__).parent
ohif_path = (f"{path}/resources/ohif")

workspace_id = spark.conf.get("spark.databricks.clusterUsageTags.clusterOwnerOrgId")
cluster_id = spark.conf.get("spark.databricks.clusterUsageTags.clusterId")

file = "app-config"

with open(f"{ohif_path}/{file}.js", "r") as config_input:
with open(f"{ohif_path}/{file}-custom.js", "w") as config_custom:
config_custom.write(
config_input.read()
.replace("{ROUTER_BASENAME}",f"/driver-proxy/o/{workspace_id}/{cluster_id}/3000/")
.replace("{PIXELS_TABLE}",table)
.replace("{ROUTER_BASENAME}",f"/driver-proxy/o/{workspace_id}/{cluster_id}/{port}/")
.replace("{PIXELS_TABLE}",os.environ["DATABRICKS_PIXELS_TABLE"])
)

# COMMAND ----------
Expand All @@ -61,22 +49,24 @@

# COMMAND ----------

from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles

from starlette.requests import Request
from starlette.responses import StreamingResponse
from starlette.responses import StreamingResponse, JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.background import BackgroundTask
from starlette.exceptions import HTTPException as StarletteHTTPException

import os
import json

import httpx
import uvicorn

app = FastAPI(title="Pixels")

async def _reverse_proxy_statements(request: Request):
client = httpx.AsyncClient(base_url=os.environ['DATABRICKS_HOST'])
client = httpx.AsyncClient(base_url=os.environ['DATABRICKS_HOST'], timeout=httpx.Timeout(30))
#Replace proxy url with right endpoint
url = httpx.URL(path=request.url.path.replace("/sqlwarehouse/",""))

Expand All @@ -98,7 +88,7 @@ async def _reverse_proxy_statements(request: Request):
)

async def _reverse_proxy_files(request: Request):
client = httpx.AsyncClient(base_url=os.environ['DATABRICKS_HOST'])
client = httpx.AsyncClient(base_url=os.environ['DATABRICKS_HOST'], timeout=httpx.Timeout(30))
#Replace proxy url with right endpoint
url = httpx.URL(path=request.url.path.replace("/sqlwarehouse/",""))

Expand All @@ -115,14 +105,29 @@ async def _reverse_proxy_files(request: Request):
background=BackgroundTask(rp_resp.aclose),
)

class DBStaticFiles(StaticFiles):
async def get_response(self, path: str, scope):
try:
return await super().get_response(path, scope)
except (HTTPException, StarletteHTTPException) as ex:
if ex.status_code == 404:
return await super().get_response("index.html", scope)
else:
raise ex

app.add_route("/sqlwarehouse/api/2.0/sql/statements/{path:path}", _reverse_proxy_statements, ["POST", "GET"])
app.add_route("/sqlwarehouse/api/2.0/fs/files/{path:path}", _reverse_proxy_files, ["GET"])
app.add_route("/sqlwarehouse/api/2.0/fs/files/{path:path}", _reverse_proxy_files, ["GET", "PUT"])
app.mount("/", DBStaticFiles(directory=f"{ohif_path}",html = True), name="ohif")

app.mount("/", StaticFiles(directory=f"{ohif_path}",html = True), name="ohif")

# COMMAND ----------

from dbtunnel import dbtunnel
dbtunnel.fastapi(app, port=3000).inject_auth().inject_env(
DATABRICKS_WAREHOUSE_ID=dbutils.widgets.get("sqlWarehouseID")
).run()
displayHTML(f"<a href='{get_proxy_url(port)}'> Click to go to OHIF Viewer!</a>")

config = uvicorn.Config(
app,
host="0.0.0.0",
port=port,
)
server = uvicorn.Server(config)
await server.serve()
61 changes: 54 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<img src=https://hls-eng-data-public.s3.amazonaws.com/img/Databricks_HLS.png width="600px">

[![DBR](https://img.shields.io/badge/DBR-13.3ML-red?logo=databricks&style=for-the-badge)](https://docs.databricks.com/release-notes/runtime/13.3ml.html)
[![DBR](https://img.shields.io/badge/DBR-14.3ML-red?logo=databricks&style=for-the-badge)](https://docs.databricks.com/release-notes/runtime/14.3ml.html)
[![CLOUD](https://img.shields.io/badge/CLOUD-ALL-blue?logo=googlecloud&style=for-the-badge)](https://cloud.google.com/databricks)
[![POC](https://img.shields.io/badge/POC-10_days-green?style=for-the-badge)](https://databricks.com/try-databricks)
---
Expand Down Expand Up @@ -89,11 +89,58 @@ Fast and multiple-layer visualization capability.
![CT_View](images/ohif_mr_view.png?raw=true)

To start the OHIF Viewer web app you need to:
- Execute the [OHIF_Viewer.py](/OHIF_Viewer.py) inside a Databricks notebook.
- Execute the [06-OHIF-Viewer](/06-OHIF-Viewer) inside a Databricks workspace.
- Set `table` parameter with full name of you pixels catalog table. Ex: `main.pixels_solacc.object_catalog`
- Set `sqlWarehouseID`parameter to execute the queries required to collect the records. It's the final section of the `HTTP path` in the `Connection details` tab. Use [Serverless](https://docs.databricks.com/en/admin/sql/warehouse-types.html#sql-warehouse-types) for best performance.![sqlWarehouseID](images/sqlWarehouseID.png?raw=true)
- Set `sqlWarehouseID`parameter to execute the queries required to collect the records. It's the final section of the `HTTP path` in the `Connection details` tab. Use [Serverless](https://docs.databricks.com/en/admin/sql/warehouse-types.html#sql-warehouse-types) for best performance.

<img src="images/sqlWarehouseID.png?raw=true" alt="sqlWarehouseID" height="300"/>

- Use the link generated in the last notebook to access the OHIF viewer page.

---
## Save measurements and segmentations
The OHIF Viewer allows you to save back in databricks the measurements and the segmentations created in the viewer.
The metadata will be stored in the object_catalog, and the generated dicom files in the volume under the path `/ohif/exports/`.

<img src="images/ohif_save_segm.png?raw=true" alt="OHIF_SAVE_SEG" height="300"/>
<img src="images/ohif_save_meas.png?raw=true" alt="OHIF_SAVE_MEAS" height="300"/>
<img src="images/ohif_save_result.png?raw=true" alt="OHIF_SAVED" height="300"/>

---
## MONAILabel Integration

[MONAILabel](https://monai.io/label.html) is an open-source tool designed for interactive medical image labeling. It supports various annotation tasks such as segmentation and classification, providing a seamless experience when integrated with viewers like OHIF that is already available in this solution accelerator.

![MONAI_BTN](images/monailabel_result.png?raw=true)
Once the server is running, you can use the OHIF Viewer to interact with your medical images. This integration allows you to leverage advanced annotation capabilities directly within your Databricks environment.

### Key Features
- Interactive Annotation: Use AI-assisted tools for efficient labeling.
- Seamless Integration: Work directly within Databricks using a web-based viewer.
- Customizable Workflows: Tailor the annotation process to fit specific research needs.

### Setup Instructions
To execute the MONAILabel server is mandatory to use a cluster with Databruck Runtime Version of `14.3 LTS ML`. For the best performance use a [GPU-Enabled compute](https://docs.databricks.com/en/compute/gpu.html#gpu-enabled-compute).
#### Start the MONAILabel server
- Execute the [05-MONAILabel](/05-MONAILabel) inside a Databricks workspace.
- Set `table` parameter with full name of you pixels catalog table. Ex: `main.pixels_solacc.object_catalog`
- Set `sqlWarehouseID`parameter to execute the queries required to collect the records. Use [Serverless](https://docs.databricks.com/en/admin/sql/warehouse-types.html#sql-warehouse-types) for best performance.
<img src="images/sqlWarehouseID.png?raw=true" alt="sqlWarehouseID" height="300"/>
#### Open the OHIF Viewer
- Execute the notebook [06-OHIF-Viewer](/06-OHIF-Viewer) to start the OHIF Viewer with the MONAILabel extension and open the generated link.
- Select the preferred CT scan study and press on `MONAI Label` button.

<img src="images/monailabel_btn.png?raw=true" alt="MONAI_BTN" height="250"/></br>
#### Connect, execute and save
- Connect the MONAILabel server using the refresh button.

<img src="images/monailabel_server.png?raw=true" alt="MONAI_SERVER" height="200"/></br>
- Execute an auto-segmentation task using the Run button and wait for the results to be displayed.

<img src="images/monailabel_autosegm.png?raw=true" alt="MONAI_AUTOSEG" height="650"/></br>
- Save the final result metadata in the catalog and the generated dicom file in the volume under the path `/ohif/exports/` using the button `Export DICOM SEG`.

This setup enhances your medical image analysis workflow by combining Databricks' computing power with MONAILabel's sophisticated annotation tools.

---
## Design
Expand Down Expand Up @@ -178,7 +225,7 @@ ___

## Installation

To run this accelerator, clone this repo into a Databricks workspace. Attach the `RUNME` notebook to any cluster running a DBR 10.4 LTS or later runtime, and execute the notebook via Run-All. A multi-step-job describing the accelerator pipeline will be created, and the link will be provided. Execute the multi-step-job to see how the pipeline runs. The job configuration is written in the RUNME notebook in json format. The cost associated with running the accelerator is the user's responsibility.
To run this accelerator, clone this repo into a Databricks workspace. Attach the `RUNME` notebook to any cluster running a DBR 14.3 LTS or later runtime, and execute the notebook via Run-All. A multi-step-job describing the accelerator pipeline will be created, and the link will be provided. Execute the multi-step-job to see how the pipeline runs. The job configuration is written in the RUNME notebook in json format. The cost associated with running the accelerator is the user's responsibility.

___
## Working with Unity Catalog (as of October 18th, 2023)
Expand All @@ -193,7 +240,7 @@ To use `databricks.pixels` with UC volumes currently requires the use of [single
___
## Licensing

&copy; 2022 Databricks, Inc. All rights reserved. The source in this notebook is provided subject to the Databricks License [https://databricks.com/db-license-source]. All included or referenced third party libraries are subject to the licenses set forth below.
&copy; 2024 Databricks, Inc. All rights reserved. The source in this notebook is provided subject to the Databricks License [https://databricks.com/db-license-source]. All included or referenced third party libraries are subject to the licenses set forth below.

| library | purpose | license | source |
|----------------------|-------------------------------------|-------------------------------|---------------------------------------------------------|
Expand All @@ -203,5 +250,5 @@ ___
| gdcm | Parse Dicom files | BSD | https://gdcm.sourceforge.net/wiki/index.php/Main_Page |
| s3fs | Resolve s3:// paths | BSD 3-Clause | https://github.com/fsspec/s3fs |
| pandas | Pandas UDFs | BSD License (BSD-3-Clause) | https://github.com/pandas-dev/pandas |
| OHIF Viewers | Medical image viewer | MIT | https://github.com/OHIF/Viewers |
| dbtunnel | Proxy to run Web UIs in Databricks notebooks | Apache-2.0 license | https://github.com/stikkireddy/dbtunnel |
| OHIF Viewer | Medical image viewer | MIT | https://github.com/OHIF/Viewers |
| MONAILabel | Intelligent open source image labeling and learning tool | Apache-2.0 license | https://github.com/Project-MONAI/MONAILabel |
58 changes: 58 additions & 0 deletions config/proxy_prep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Databricks notebook source
import os
from dbruntime.databricks_repl_context import get_context
from databricks.sdk import WorkspaceClient

ctx = get_context()
w = WorkspaceClient()


# COMMAND ----------

def init_widgets():
dbutils.widgets.text("table", "main.pixels_solacc.object_catalog", label="1.0 Catalog Schema Table to store object metadata into")
dbutils.widgets.text("sqlWarehouseID", "", label="2.0 SQL Warehouse")
sql_warehouse_id = dbutils.widgets.get("sqlWarehouseID")
table = dbutils.widgets.get("table")
return sql_warehouse_id, table

# COMMAND ----------

def init_env():
sql_warehouse_id = dbutils.widgets.get("sqlWarehouseID")
table = dbutils.widgets.get("table")

if not spark.catalog.tableExists(table):
raise Exception("The configured table does not exist!")

if sql_warehouse_id == "":
raise Exception("SQL Warehouse ID is mandatory!")
else:
wh = w.warehouses.get(id=sql_warehouse_id)
print(f"Using '{wh.as_dict()['name']}' as SQL Warehouse")

os.environ["DATABRICKS_TOKEN"] = ctx.apiToken
os.environ["DATABRICKS_WAREHOUSE_ID"] = sql_warehouse_id
os.environ["DATABRICKS_HOST"] = f"https://{ctx.browserHostName}"
os.environ["DATABRICKS_PIXELS_TABLE"] = table

# COMMAND ----------

def get_proxy_url(port:int = 8000):
host_name = ctx.browserHostName
ending = f"driver-proxy/o/{get_context().workspaceId}/{get_context().clusterId}/{port}/"

azure = "azuredatabricks.net"
gcp = "gcp.databricks.com"
aws = "cloud.databricks.com"

if azure in host_name:
shard = int(get_context().workspaceId) % 20
return f"https://adb-dp-{get_context().workspaceId}.{shard}.{azure}/{ending}"
elif gcp in host_name:
shard = int(get_context().workspaceId) % 10
return f"https://dp-{get_context().workspaceId}.{shard}.{gcp}/{ending}"
elif aws in host_name:
dbc_host = '.'.join(host_name.split(".")[1:])
return f"https://dbc-dp-{get_context().workspaceId}.{dbc_host}/{ending}"

Loading

0 comments on commit 1b7c92b

Please sign in to comment.