Skip to content

Commit 14e88a7

Browse files
CVAT deid (#999)
* Fix network for tooltracking based on monai rc2 + optimize transform Signed-off-by: Sachidanand Alle <[email protected]> * fix nan issue for variance scoring + optimize post transform Signed-off-by: Sachidanand Alle <[email protected]> * fix comment Signed-off-by: Sachidanand Alle <[email protected]> * fix deid logic Signed-off-by: Sachidanand Alle <[email protected]> * fix normalize logic for tool Signed-off-by: Sachidanand Alle <[email protected]> * fix args Signed-off-by: Sachidanand Alle <[email protected]> Signed-off-by: Sachidanand Alle <[email protected]>
1 parent 421ab05 commit 14e88a7

File tree

14 files changed

+128
-34
lines changed

14 files changed

+128
-34
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# to use different version of MONAI pass `--build-arg MONAI_IMAGE=...`
1515
# to exclude ORTHANC pass `--build-arg ORTHANC=false`
1616

17-
ARG MONAI_IMAGE=projectmonai/monai:1.0.0rc1
17+
ARG MONAI_IMAGE=projectmonai/monai:1.0.0rc3
1818
ARG ORTHANC=false
1919
ARG BUILD_OHIF=true
2020

monailabel/datastore/cvat.py

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import tempfile
1717
import time
1818

19+
import numpy as np
1920
import requests
2021
from PIL import Image
2122
from requests.auth import HTTPBasicAuth
@@ -39,6 +40,7 @@ def __init__(
3940
image_quality=70,
4041
labels=None,
4142
normalize_label=True,
43+
segment_size=1,
4244
**kwargs,
4345
):
4446
default_labels = [
@@ -47,15 +49,17 @@ def __init__(
4749
{"name": "OutBody", "attributes": [], "color": "#0000ff"},
4850
]
4951
labels = labels if labels else default_labels
50-
labels = json.loads(labels) if isinstance(labels, str) else self.json()
52+
labels = json.loads(labels) if isinstance(labels, str) else labels
5153

5254
self.api_url = api_url.rstrip("/").strip()
5355
self.auth = HTTPBasicAuth(username, password) if username else None
5456
self.project = project
5557
self.task_prefix = task_prefix
5658
self.image_quality = image_quality
5759
self.labels = labels
60+
self.label_map = {l["name"]: idx for idx, l in enumerate(labels, start=1)}
5861
self.normalize_label = normalize_label
62+
self.segment_size = segment_size
5963

6064
logger.info(f"CVAT:: API URL: {api_url}")
6165
logger.info(f"CVAT:: UserName: {username}")
@@ -65,6 +69,7 @@ def __init__(
6569
logger.info(f"CVAT:: Image Quality: {image_quality}")
6670
logger.info(f"CVAT:: Labels: {labels}")
6771
logger.info(f"CVAT:: Normalize Label: {normalize_label}")
72+
logger.info(f"CVAT:: Segment Size: {normalize_label}")
6873

6974
super().__init__(datastore_path=datastore_path, **kwargs)
7075

@@ -117,7 +122,10 @@ def get_cvat_task_id(self, project_id, create):
117122
task_name = f"{self.task_prefix}_{version}"
118123
logger.info(f"Creating new CVAT Task: {task_name}; project: {self.project}")
119124

120-
body = {"name": task_name, "labels": [], "project_id": project_id, "subset": "Train", "segment_size": 1}
125+
body = {"name": task_name, "labels": [], "project_id": project_id, "subset": "Train"}
126+
if self.segment_size:
127+
body["segment_size"] = self.segment_size
128+
121129
task = requests.post(f"{self.api_url}/api/tasks", auth=self.auth, json=body).json()
122130
logger.debug(task)
123131
task_id = task["id"]
@@ -157,6 +165,18 @@ def trigger_automation(self, function):
157165
r = requests.post(f"{self.api_url}/api/lambda/requests?org=", json=body, auth=self.auth).json()
158166
logger.info(r)
159167

168+
def _load_labelmap_txt(self, file):
169+
labelmap = {}
170+
if os.path.exists(file):
171+
with open(file) as f:
172+
for line in f.readlines():
173+
if line and not line.startswith("#"):
174+
fields = line.split(":")
175+
name = fields[0]
176+
rgb = tuple(int(c) for c in fields[1].split(","))
177+
labelmap[name] = rgb
178+
return labelmap
179+
160180
def download_from_cvat(self, max_retry_count=5, retry_wait_time=10):
161181
if self.task_status() != "completed":
162182
logger.info("No Tasks exists with completed status to refresh/download the final labels")
@@ -190,10 +210,19 @@ def download_from_cvat(self, max_retry_count=5, retry_wait_time=10):
190210

191211
dest = os.path.join(final_labels, f)
192212
if self.normalize_label:
193-
Image.open(label).convert("L").point(lambda x: 0 if x < 128 else 255, "1").save(dest)
213+
img = np.array(Image.open(label))
214+
mask = np.zeros_like(img)
215+
216+
labelmap = self._load_labelmap_txt(os.path.join(tmp_folder, "labelmap.txt"))
217+
for name, color in labelmap.items():
218+
if name in self.label_map:
219+
idx = self.label_map.get(name)
220+
mask[np.all(img == color, axis=-1)] = idx
221+
Image.fromarray(mask[:, :, 0]).save(dest) # single channel
222+
logger.info(f"Copy Final Label: {label} to {dest}; unique: {np.unique(mask)}")
194223
else:
195224
Image.open(label).save(dest)
196-
logger.info(f"Copy Final Label: {label} to {dest}")
225+
logger.info(f"Copy Final Label: {label} to {dest}")
197226

198227
# Rename after consuming/downloading the labels
199228
patch_url = f"{self.api_url}/api/tasks/{task_id}"
@@ -206,3 +235,51 @@ def download_from_cvat(self, max_retry_count=5, retry_wait_time=10):
206235
logger.error(f"{retry} => Failed to download...")
207236
retry_count = retry_count + 1
208237
return None
238+
239+
240+
def main():
241+
from pathlib import Path
242+
243+
from monailabel.config import settings
244+
245+
settings.MONAI_LABEL_DATASTORE_AUTO_RELOAD = False
246+
settings.MONAI_LABEL_DATASTORE_FILE_EXT = ["*.png", "*.jpg", "*.jpeg", ".xml"]
247+
settings.MONAI_LABEL_DATASTORE = "cvat"
248+
settings.MONAI_LABEL_DATASTORE_URL = "http://10.117.19.88:8080"
249+
settings.MONAI_LABEL_DATASTORE_USERNAME = "sachi"
250+
settings.MONAI_LABEL_DATASTORE_PASSWORD = "sachi"
251+
252+
os.putenv("MASTER_ADDR", "127.0.0.1")
253+
os.putenv("MASTER_PORT", "1234")
254+
255+
logging.basicConfig(
256+
level=logging.INFO,
257+
format="[%(asctime)s] [%(process)s] [%(threadName)s] [%(levelname)s] (%(name)s:%(lineno)d) - %(message)s",
258+
datefmt="%Y-%m-%d %H:%M:%S",
259+
force=True,
260+
)
261+
262+
home = str(Path.home())
263+
studies = f"{home}/Dataset/picked/all"
264+
265+
ds = CVATDatastore(
266+
datastore_path=studies,
267+
api_url=settings.MONAI_LABEL_DATASTORE_URL,
268+
username=settings.MONAI_LABEL_DATASTORE_USERNAME,
269+
password=settings.MONAI_LABEL_DATASTORE_PASSWORD,
270+
project="MONAILabel",
271+
task_prefix="ActiveLearning_Iteration",
272+
image_quality=70,
273+
labels=None,
274+
normalize_label=True,
275+
segment_size=0,
276+
extensions=settings.MONAI_LABEL_DATASTORE_FILE_EXT,
277+
auto_reload=settings.MONAI_LABEL_DATASTORE_AUTO_RELOAD,
278+
)
279+
ds.download_from_cvat()
280+
281+
# studies = f"{home}/Dataset/Holoscan/flattened/images"
282+
283+
284+
if __name__ == "__main__":
285+
main()

plugins/cvat/endoscopy/deid.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ metadata:
1818
framework: pytorch
1919
spec: |
2020
[
21-
{ "id": 1, "name": "Tool" }
21+
{ "id": 1, "name": "InBody" },
22+
{ "id": 2, "name": "OutBody" }
2223
]
2324
2425
spec:
25-
description: A pre-trained classification model for Endoscopy to flag if image contains Tool
26+
description: A pre-trained classification model for Endoscopy to flag if image follows InBody or OutBody
2627
runtime: 'python:3.8'
2728
handler: main:handler
2829
eventTimeout: 30s

plugins/cvat/main.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,25 @@ def handler(context, event):
8989
prediction = json_data["params"].get("prediction")
9090
if prediction:
9191
context.logger.info(f"(Classification) Prediction: {prediction}")
92+
93+
# CVAT Limitation:: tag is not yet supported https://github.com/opencv/cvat/issues/4212
94+
# CVAT Limitation:: select highest score and create bbox to represent as tag
95+
e = None
9296
for element in prediction:
9397
if element["score"] > 0:
94-
# CVAT Limitation:: tag is not yet supported https://github.com/opencv/cvat/issues/4212
95-
results.append(
96-
{
97-
"label": element["label"],
98-
"confidence": element["score"],
99-
"type": "rectangle",
100-
"points": [0, 0, image_np.shape[0] - 1, image_np.shape[1] - 1],
101-
}
102-
)
98+
e = element if e is None or element["score"] > e["score"] else e
99+
context.logger.info(f"New Max Element: {e}")
100+
101+
context.logger.info(f"Final Element with Max Score: {e}")
102+
if e:
103+
results.append(
104+
{
105+
"label": e["label"],
106+
"confidence": e["score"],
107+
"type": "rectangle",
108+
"points": [0, 0, image_np.shape[0] - 1, image_np.shape[1] - 1],
109+
}
110+
)
103111
context.logger.info(f"(Classification) Results: {results}")
104112
else:
105113
interactor = strtobool(os.environ.get("INTERACTOR_MODEL", "false"))

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# limitations under the License.
1111

1212
torch>=1.7
13-
monai[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, openslide, fire]>=1.0.0rc2
13+
monai[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, openslide, fire]>=1.0.0rc3
1414
uvicorn==0.17.6
1515
pydantic==1.9.1
1616
python-dotenv==0.20.0

sample-apps/endoscopy/lib/configs/deid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def init(self, name: str, model_dir: str, conf: Dict[str, str], planner: Any, **
3434
super().init(name, model_dir, conf, planner, **kwargs)
3535

3636
# Labels
37-
self.labels = {"Tool": 0}
37+
self.labels = {"InBody": 0, "OutBody": 1}
3838

3939
# Model Files
4040
self.path = [

sample-apps/endoscopy/lib/infers/deepedit.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,7 @@ def post_transforms(self, data=None) -> Sequence[Callable]:
7676
return [
7777
EnsureTyped(keys="pred", device=data.get("device") if data else None),
7878
Activationsd(keys="pred", sigmoid=True),
79-
AsDiscreted(keys="pred", threshold_values=True, logit_thresh=0.5),
80-
# NormalizeLabeld(keys="pred"),
81-
# DumpImagePrediction2Dd(image_path="img.png", pred_path="pred.png"),
79+
AsDiscreted(keys="pred", threshold=0.5),
8280
Restored(keys="pred", ref_image="image"),
8381
SqueezeDimd(keys="pred"),
8482
ToNumpyd(keys="pred", dtype=np.uint8),

sample-apps/endoscopy/lib/infers/deid.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import numpy as np
1616
import torch
1717
from monai.inferers import Inferer, SimpleInferer
18-
from monai.transforms import AsChannelFirstd, AsDiscreted, CastToTyped, EnsureTyped, NormalizeIntensityd, Resized
18+
from monai.transforms import AsChannelFirstd, AsDiscreted, CastToTyped, NormalizeIntensityd, Resized
1919

2020
from monailabel.interfaces.tasks.infer import InferTask, InferType
2121
from monailabel.transform.pre import LoadImageExd
@@ -57,7 +57,6 @@ def info(self) -> Dict[str, Any]:
5757
def pre_transforms(self, data=None) -> Sequence[Callable]:
5858
return [
5959
LoadImageExd(keys="image", dtype=np.uint8),
60-
EnsureTyped(keys="image", device=data.get("device") if data else None),
6160
AsChannelFirstd(keys="image"),
6261
Resized(keys="image", spatial_size=(256, 256), mode="bilinear"),
6362
CastToTyped(keys="image", dtype=torch.float32),
@@ -69,7 +68,6 @@ def inferer(self, data=None) -> Inferer:
6968

7069
def post_transforms(self, data=None) -> Sequence[Callable]:
7170
return [
72-
EnsureTyped(keys="pred", device=data.get("device") if data else None),
7371
AsDiscreted(keys="pred", argmax=True, to_onehot=2),
7472
]
7573

sample-apps/endoscopy/lib/trainers/deepedit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def train_pre_transforms(self, context: Context):
106106
def train_post_transforms(self, context: Context):
107107
return [
108108
Activationsd(keys="pred", sigmoid=True),
109-
AsDiscreted(keys="pred", threshold_values=True, logit_thresh=0.5),
109+
AsDiscreted(keys="pred", threshold=0.5),
110110
]
111111

112112
def val_inferer(self, context: Context):

sample-apps/endoscopy/lib/trainers/deid.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def loss_function(self, context: Context):
6363
def train_pre_transforms(self, context: Context):
6464
return [
6565
LoadImaged(keys=("image", "label"), dtype=np.uint8),
66-
LabelToBinaryClassd(keys="label"),
66+
LabelToBinaryClassd(keys="label", offset=2),
6767
ToTensord(keys=("image", "label")),
6868
AsChannelFirstd("image"),
6969
Resized(keys="image", spatial_size=(256, 256), mode="bilinear"),
@@ -84,7 +84,7 @@ def train_post_transforms(self, context: Context):
8484
def val_pre_transforms(self, context: Context):
8585
return [
8686
LoadImaged(keys=("image", "label"), dtype=np.uint8),
87-
LabelToBinaryClassd(keys="label"),
87+
LabelToBinaryClassd(keys="label", offset=2),
8888
ToTensord(keys=("image", "label")),
8989
AsChannelFirstd("image"),
9090
Resized(keys="image", spatial_size=(256, 256), mode="bilinear"),

0 commit comments

Comments
 (0)