Skip to content

Commit 7e7a126

Browse files
committed
- 优化dcm支持
1 parent 0c55e31 commit 7e7a126

File tree

4 files changed

+54
-126
lines changed

4 files changed

+54
-126
lines changed

ISAT/annotation.py

Lines changed: 14 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -6,56 +6,10 @@
66
import numpy as np
77
from json import load, dump
88
from typing import List, Union
9-
import pydicom
9+
from ISAT.utils.dicom import load_dcm_as_image
1010

1111
__all__ = ['Object', 'Annotation']
1212

13-
14-
def get_windowed_image(ds):
15-
# Apply Rescale Slope and Rescale Intercept if they exist
16-
pixel_array = ds.pixel_array.astype(float)
17-
rescale_slope = ds.get("RescaleSlope", 1)
18-
rescale_intercept = ds.get("RescaleIntercept", 0)
19-
if rescale_slope != 1 or rescale_intercept != 0:
20-
pixel_array = pixel_array * rescale_slope + rescale_intercept
21-
22-
# Check for windowing information in the DICOM metadata
23-
window_center = ds.get("WindowCenter", None)
24-
window_width = ds.get("WindowWidth", None)
25-
26-
if window_center is None or window_width is None:
27-
# If no windowing info, use the full dynamic range
28-
window_min = pixel_array.min()
29-
window_max = pixel_array.max()
30-
else:
31-
# Handle possible multi-valued tags by taking the first value
32-
if isinstance(window_center, pydicom.multival.MultiValue):
33-
window_center = window_center[0]
34-
if isinstance(window_width, pydicom.multival.MultiValue):
35-
window_width = window_width[0]
36-
37-
window_min = window_center - window_width / 2
38-
window_max = window_center + window_width / 2
39-
40-
# Apply windowing
41-
pixel_array = np.clip(pixel_array, window_min, window_max)
42-
43-
# Normalize to 0-255
44-
if window_max > window_min:
45-
pixel_array = ((pixel_array - window_min) / (window_max - window_min)) * 255.0
46-
else: # Handle case where all pixels are the same
47-
pixel_array.fill(128)
48-
49-
# Handle Photometric Interpretation
50-
photometric_interpretation = ds.get("PhotometricInterpretation", "MONOCHROME2")
51-
if photometric_interpretation == "MONOCHROME1":
52-
pixel_array = 255.0 - pixel_array
53-
54-
# Convert to 8-bit unsigned integer
55-
image_8bit = pixel_array.astype(np.uint8)
56-
57-
return image_8bit
58-
5913
class Object:
6014
r"""A class to represent an annotation object.
6115
@@ -104,48 +58,23 @@ def __init__(self, image_path:str, label_path:str):
10458
self.img_name = img_name
10559
self.label_path = label_path
10660
self.note = ''
107-
self.image_path = image_path
108-
self._img_data = None
10961

110-
# Defer image loading to get_img_data()
111-
self.width = 0
112-
self.height = 0
113-
self.depth = 0
62+
if image_path.lower().endswith('.dcm'):
63+
image = np.array(load_dcm_as_image(image_path))
64+
else:
65+
image = np.array(Image.open(image_path))
66+
if image.ndim == 3:
67+
self.height, self.width, self.depth = image.shape
68+
elif image.ndim == 2:
69+
self.height, self.width = image.shape
70+
self.depth = 0
71+
else:
72+
self.height, self.width, self.depth = image.shape[:, :3]
73+
print('Warning: Except image has 2 or 3 ndim, but get {}.'.format(image.ndim))
74+
del image
11475

11576
self.objects:List[Object, ] = []
11677

117-
def get_img_data(self, to_rgb=False):
118-
if self._img_data is None:
119-
if self.image_path.lower().endswith('.dcm'):
120-
ds = pydicom.dcmread(self.image_path)
121-
image_data = get_windowed_image(ds)
122-
self._img_data = np.stack([image_data, image_data, image_data], axis=-1)
123-
else:
124-
self._img_data = Image.open(self.image_path)
125-
126-
if self.width == 0 or self.height == 0:
127-
if isinstance(self._img_data, np.ndarray):
128-
image = self._img_data
129-
if image.ndim == 3:
130-
self.height, self.width, self.depth = image.shape
131-
elif image.ndim == 2:
132-
self.height, self.width = image.shape
133-
self.depth = 1
134-
else: # PIL Image
135-
self.width, self.height = self._img_data.size
136-
self.depth = len(self._img_data.getbands())
137-
138-
if to_rgb:
139-
if isinstance(self._img_data, np.ndarray):
140-
return self._img_data # DICOM data is already RGB numpy array
141-
else: # PIL Image
142-
return np.array(self._img_data.convert('RGB'))
143-
else:
144-
if isinstance(self._img_data, np.ndarray):
145-
return self._img_data
146-
else: # PIL Image
147-
return np.array(self._img_data)
148-
14978
def load_annotation(self):
15079
r"""
15180
Load annotation from self.label_path

ISAT/widgets/canvas.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from PyQt5 import QtWidgets, QtGui, QtCore
55
from ISAT.widgets.polygon import Polygon, Vertex, PromptPoint, Line, Rect
66
from ISAT.configs import STATUSMode, DRAWMode, CONTOURMode
7+
from ISAT.utils.dicom import load_dcm_as_image
78
import numpy as np
89
import cv2
910
import time # 拖动鼠标描点
1011
import shapely
11-
import pydicom
1212

1313

1414
class AnnotationScene(QtWidgets.QGraphicsScene):
@@ -90,8 +90,13 @@ def load_image(self, image_path: str):
9090
if self.mainwindow.use_segment_anything:
9191
self.mainwindow.segany.reset_image()
9292

93-
self.image_data = self.mainwindow.current_label.get_img_data(to_rgb=True)
94-
93+
if image_path.lower().endswith('.dcm'):
94+
image = load_dcm_as_image(image_path)
95+
else:
96+
image = Image.open(image_path)
97+
if self.mainwindow.can_be_annotated:
98+
image = image.convert('RGB')
99+
self.image_data = np.array(image)
95100
self.image_item = QtWidgets.QGraphicsPixmapItem()
96101
self.image_item.setZValue(0)
97102
self.addItem(self.image_item)
@@ -101,9 +106,10 @@ def load_image(self, image_path: str):
101106

102107
height, width, channel = self.image_data.shape
103108
bytes_per_line = channel * width
104-
q_image = QtGui.QImage(self.image_data.data, width, height, bytes_per_line,
109+
q_image = QtGui.QImage(self.image_data.tobytes(), width, height, bytes_per_line,
105110
QtGui.QImage.Format_RGB888)
106111
self.image_item.setPixmap(QtGui.QPixmap.fromImage(q_image))
112+
# self.image_item.setPixmap(QtGui.QPixmap(image_path))
107113
self.setSceneRect(self.image_item.boundingRect())
108114
self.change_mode_to_view()
109115

@@ -599,7 +605,7 @@ def copy_item(self):
599605
x, y = point.x(), point.y()
600606
self.current_graph.addPoint(QtCore.QPointF(x, y))
601607

602-
self.current_graph.set_drawed(item.category, item.group, item.iscrowd, item.note, item.color, item.zValue())
608+
self.current_graph.set_drawed(item.category, item.group, item.iscrowd, item.note, item.color, int(item.zValue()))
603609
self.mainwindow.polygons.insert(index, self.current_graph)
604610
self.mainwindow.annos_dock_widget.listwidget_add_polygon(self.current_graph)
605611
item.setSelected(False)
@@ -1087,7 +1093,7 @@ def update_mask(self):
10871093
else:
10881094
mask_image = np.zeros(self.image_data.shape, dtype=np.uint8)
10891095
mask_image = cv2.addWeighted(self.image_data, 1, mask_image, 0, 0)
1090-
mask_image = QtGui.QImage(mask_image[:], mask_image.shape[1], mask_image.shape[0], mask_image.shape[1] * 3,
1096+
mask_image = QtGui.QImage(mask_image.tobytes(), mask_image.shape[1], mask_image.shape[0], mask_image.shape[1] * 3,
10911097
QtGui.QImage.Format_RGB888)
10921098
mask_pixmap = QtGui.QPixmap(mask_image)
10931099
if self.mask_item is not None:

ISAT/widgets/mainwindow.py

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from ISAT.configs import STATUSMode, MAPMode, load_config, save_config, CONFIG_FILE, SOFTWARE_CONFIG_FILE, CHECKPOINT_PATH, ISAT_ROOT, CONTOURMode
2626
from ISAT.annotation import Object, Annotation
2727
from ISAT.widgets.polygon import Polygon, PromptPoint
28+
from ISAT.utils.dicom import load_dcm_as_image
2829
import os
2930
from PIL import Image
3031
import functools
@@ -39,8 +40,6 @@
3940
from skimage.draw.draw import polygon
4041
import requests
4142
import orjson
42-
import pydicom
43-
from ISAT.annotation import get_windowed_image
4443

4544

4645
class QtBoxStyleProgressBar(QtWidgets.QProgressBar):
@@ -209,7 +208,10 @@ def run(self):
209208

210209
image_path = os.path.join(self.mainwindow.image_root, self.mainwindow.files_list[index])
211210

212-
image_data = self.mainwindow.current_label.get_img_data(to_rgb=True)
211+
if image_path.lower().endswith('.dcm'):
212+
image_data = np.array(load_dcm_as_image(image_path).convert('RGB'))
213+
else:
214+
image_data = np.array(Image.open(image_path).convert('RGB'))
213215
try:
214216
features, original_size, input_size = self.sam_encoder(image_data)
215217
except Exception as e:
@@ -1182,31 +1184,16 @@ def show_image(self, index: int, zoomfit: bool=True):
11821184
self.plugin_manager_dialog.trigger_before_image_open(file_path)
11831185

11841186
if file_path.lower().endswith('.dcm'):
1185-
self.can_be_annotated = True
1187+
image_data = load_dcm_as_image(file_path)
11861188
else:
1187-
try:
1188-
image = Image.open(file_path)
1189-
png_palette = image.getpalette()
1190-
if png_palette is not None and file_path.endswith('.png'):
1191-
self.statusbar.showMessage('This image might be a label image in VOC format.')
1192-
self.can_be_annotated = False
1193-
else:
1194-
self.can_be_annotated = True
1195-
except Exception as e:
1196-
# stausbar show error
1197-
self.statusbar.showMessage(str(e), 5000)
1198-
return
1189+
image_data = Image.open(file_path)
11991190

1200-
# load label
1201-
if self.can_be_annotated:
1202-
self.current_group = 1
1203-
_, name = os.path.split(file_path)
1204-
label_path = os.path.join(self.label_root, '.'.join(name.split('.')[:-1]) + '.json')
1205-
self.current_label = Annotation(file_path, label_path)
1206-
# 载入数据
1207-
self.current_label.load_annotation()
1208-
# get image data and info
1209-
self.current_label.get_img_data()
1191+
png_palette = image_data.getpalette()
1192+
if png_palette is not None and file_path.endswith('.png'):
1193+
self.statusbar.showMessage('This image might be a label image in VOC format.')
1194+
self.can_be_annotated = False
1195+
else:
1196+
self.can_be_annotated = True
12101197

12111198
if self.can_be_annotated:
12121199
self.actionPolygon.setEnabled(True)
@@ -1231,22 +1218,28 @@ def show_image(self, index: int, zoomfit: bool=True):
12311218
self.view.zoomfit()
12321219

12331220
# 判断图像是否旋转
1234-
if not file_path.lower().endswith('.dcm'):
1235-
image_data = Image.open(file_path)
1236-
exif_info = image_data.getexif()
1237-
if exif_info and exif_info.get(274, 1) != 1:
1238-
warning_info = '这幅图像包含EXIF元数据,且图像的方向已被旋转.\n建议去除EXIF信息后再进行标注\n你可以使用[菜单栏]-[工具]-[处理exif标签]功能处理图像的旋转问题。'\
1239-
if self.cfg['software']['language'] == 'zh' \
1240-
else 'This image has EXIF metadata, and the image orientation is rotated.\nSuggest labeling after removing the EXIF metadata.\nYou can use the function of [Process EXIF tag] in [Tools] in [Menu bar] to deal with the problem of images.'
1241-
QtWidgets.QMessageBox.warning(self, 'Warning', warning_info, QtWidgets.QMessageBox.Ok)
1221+
exif_info = image_data.getexif()
1222+
if exif_info and exif_info.get(274, 1) != 1:
1223+
warning_info = '这幅图像包含EXIF元数据,且图像的方向已被旋转.\n建议去除EXIF信息后再进行标注\n你可以使用[菜单栏]-[工具]-[处理exif标签]功能处理图像的旋转问题。'\
1224+
if self.cfg['software']['language'] == 'zh' \
1225+
else 'This image has EXIF metadata, and the image orientation is rotated.\nSuggest labeling after removing the EXIF metadata.\nYou can use the function of [Process EXIF tag] in [Tools] in [Menu bar] to deal with the problem of images.'
1226+
QtWidgets.QMessageBox.warning(self, 'Warning', warning_info, QtWidgets.QMessageBox.Ok)
12421227

12431228
if self.use_segment_anything and self.can_be_annotated:
12441229
self.segany.reset_image()
12451230
self.seganythread.index = index
12461231
self.seganythread.start()
12471232
self.SeganyEnabled()
12481233

1234+
# load label
12491235
if self.can_be_annotated:
1236+
self.current_group = 1
1237+
_, name = os.path.split(file_path)
1238+
label_path = os.path.join(self.label_root, '.'.join(name.split('.')[:-1]) + '.json')
1239+
self.current_label = Annotation(file_path, label_path)
1240+
# 载入数据
1241+
self.current_label.load_annotation()
1242+
12501243
for object in self.current_label.objects:
12511244
try:
12521245
group = int(object.group)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ fuzzywuzzy
1616
python-Levenshtein
1717
iopath
1818
orjson
19-
pydicom
19+
pydicom

0 commit comments

Comments
 (0)