Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 53 spline segmentation export #63

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion annotationweb/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class TaskForm(forms.ModelForm):
class Meta:
model = Task
fields = ['name', 'dataset', 'show_entire_sequence', 'frames_before',
'frames_after', 'auto_play', 'user_frame_selection', 'annotate_single_frame', 'shuffle_videos', 'type', 'label', 'user', 'description']
'frames_after', 'auto_play', 'user_frame_selection', 'annotate_single_frame', 'shuffle_videos', 'type', 'label', 'user', 'description', 'large_image_layout']

# def clean(self):
# cleaned_data = super(TaskForm, self).clean()
Expand Down
4 changes: 3 additions & 1 deletion annotationweb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ class Task(models.Model):
CARDIAC_PLAX_SEGMENTATION = 'cardiac_plax_segmentation'
CARDIAC_ALAX_SEGMENTATION = 'cardiac_alax_segmentation'
SPLINE_SEGMENTATION = 'spline_segmentation'
VIDEO_ANNOTATION = 'video_annotation'
TASK_TYPES = (
(CLASSIFICATION, 'Classification'),
(BOUNDING_BOX, 'Bounding box'),
(LANDMARK, 'Landmark'),
(CARDIAC_SEGMENTATION, 'Cardiac apical segmentation'),
(CARDIAC_PLAX_SEGMENTATION, 'Cardiac PLAX segmentation'),
(CARDIAC_ALAX_SEGMENTATION, 'Cardiac ALAX segmentation'),
(SPLINE_SEGMENTATION, 'Spline segmentation')
(SPLINE_SEGMENTATION, 'Spline segmentation'),
(VIDEO_ANNOTATION, 'Video annotation')
)

name = models.CharField(max_length=200)
Expand Down
3 changes: 2 additions & 1 deletion annotationweb/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'lungdata-medtech.sintef.no']


# Application definition
Expand All @@ -50,6 +50,7 @@
'spline_segmentation',
'cardiac_parasternal_long_axis',
'cardiac_apical_long_axis',
'video_annotation',
'django_otp',
'django_otp.plugins.otp_totp',
]
Expand Down
3 changes: 2 additions & 1 deletion annotationweb/static/annotationweb/annotationweb.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function incrementFrame() {
$('#slider').slider('value', g_currentFrameNr); // Update slider
$('#currentFrame').text(g_currentFrameNr);
redrawSequence();
window.setTimeout(incrementFrame, 50);
window.setTimeout(incrementFrame, 100);
}

function setPlayButton(play) {
Expand Down Expand Up @@ -280,6 +280,7 @@ function loadSequence(image_sequence_id, start_frame, nrOfFrames, show_entire_se
}
g_startFrame = start;
g_sequenceLength = end-start;
console.log("Start frame = " + toString(g_startFrame) + ", sequence length = " + toString(g_sequenceLength));

// Create slider
$("#slider").slider(
Expand Down
5 changes: 4 additions & 1 deletion annotationweb/templates/annotationweb/do_task.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ <h3>Actions</h3>
{% if next_image_id %}
<button onclick="javascript:changeImage('{% url 'annotate' task.id next_image_id %}?{{ request.GET.urlencode }}');">Next</button>
{% endif %}
<button id="clearButton" title="Clear">Clear</button>
<button id="clearButton" title="Clear active annotation object from frame">Clear</button>
<button id="rejectButton" alt="By rejecting this image, it is removed from the dataset. You may write a comment below of why it was rejected." title="Save as rejected">Reject</button>
<button id="saveButton" title="Save">Save</button>
<button id="imageListButton" onclick="javascript:window.location.href='{% url 'task' task.id %}'" title="Image list">List</button>
Expand All @@ -86,6 +86,9 @@ <h3>Annotation</h3>
</form>
</div>

{% block CopyContentButton %}
{% endblock CopyContentButton%}

<div id="labelButtons">
{% block label_buttons %}
<div class="flexButtons">
Expand Down
3 changes: 2 additions & 1 deletion annotationweb/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
path('cardiac/', include('cardiac.urls')),
path('cardiac-plax/', include('cardiac_parasternal_long_axis.urls')),
path('cardiac-alax/', include('cardiac_apical_long_axis.urls')),
path('spline-segmentation/', include('spline_segmentation.urls'))
path('spline-segmentation/', include('spline_segmentation.urls')),
path('video-annotation/', include('video_annotation.urls'))
]

# This is for making statics in a development environment
Expand Down
2 changes: 2 additions & 0 deletions annotationweb/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,8 @@ def get_redirection(task):
return 'cardiac_apical_long_axis:segment_image'
elif task.type == Task.SPLINE_SEGMENTATION:
return 'spline_segmentation:segment_image'
elif task.type == Task.VIDEO_ANNOTATION:
return 'video_annotation:process_image'


# @register.simple_tag
Expand Down
18 changes: 18 additions & 0 deletions boundingbox/static/boundingbox/boundingbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,21 @@ function redrawSequence() {
g_context.drawImage(g_sequence[index], 0, 0, g_canvasWidth, g_canvasHeight);
redraw();
}

function copyToNext() {
// TODO: Check content of g_boxes[g_currentFrameNr+1] -> Is it empty/exist? Overwrite?
if (g_currentFrameNr < g_sequenceLength + 1) {
var boxes_to_copy = g_boxes[g_currentFrameNr];
for (var i = 0; i < boxes_to_copy.length; i++) {
addBox(g_currentFrameNr + 1, boxes_to_copy[i].x, boxes_to_copy[i].y,
boxes_to_copy[i].x + boxes_to_copy[i].width,
boxes_to_copy[i].y + boxes_to_copy[i].height,
boxes_to_copy[i].label_id);
}
//console.log(boxes_to_copy)
// Uncomment the next two lines if you want a confirmation message.
// Note that this message will stay until the site is refreshed
// var messageBox = document.getElementById("message");
// messageBox.innerHTML = '<span class="info">Content copied to the next frame!</span>';
}
}
6 changes: 6 additions & 0 deletions boundingbox/templates/boundingbox/process_image.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@

{% endblock task_javascript %}

{% block CopyContentButton %}
<div id="CopyContentButton">
<button type="button" onclick=copyToNext()>Copy to next frame</button>
</div>
{% endblock %}

{% block task_instructions %}
{# TODO Put task instructions here.. #}
{% endblock task_instructions %}
16 changes: 14 additions & 2 deletions common/metaimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def __init__(self, filename=None, data=None, channels=False):
self.attributes = {}
self.attributes['ElementSpacing'] = [1, 1, 1]
self.attributes['ElementNumberOfChannels'] = 1
self.attributes['Offset'] = [0, 0]
if filename is not None:
self.read(filename)
else:
Expand Down Expand Up @@ -48,6 +49,8 @@ def read(self, filename):
self.attributes[parts[0].strip()] = parts[1].strip()
if parts[0].strip() == 'ElementSpacing':
self.attributes['ElementSpacing'] = [float(x) for x in self.attributes['ElementSpacing'].split()]
if parts[0].strip() == 'Offset':
self.attributes['Offset'] = [float(x) for x in self.attributes['Offset'].split()]

dims = self.attributes['DimSize'].split(' ')
if len(dims) == 2:
Expand All @@ -73,7 +76,7 @@ def read(self, filename):
# Read uncompressed raw file (.raw)
self.data = np.fromfile(os.path.join(base_path, self.attributes['ElementDataFile']), dtype=np.uint8)


# TODO: are L80-84 duplicates of L55-59?
dims = self.attributes['DimSize'].split(' ')
if len(dims) == 2:
self.dim_size = (int(dims[0]), int(dims[1]))
Expand Down Expand Up @@ -114,6 +117,14 @@ def set_spacing(self, spacing):
def get_spacing(self):
return self.attributes['ElementSpacing']

def set_origin(self, origin):
if len(origin) != 2 and len(origin) != 3:
raise ValueError('Origin must have 2 or 3 components')
self.attributes['Offset'] = origin

def get_origin(self):
return self.attributes['Offset']

def get_metaimage_type(self):
np_type = self.data.dtype
if np_type == np.float32:
Expand Down Expand Up @@ -147,12 +158,13 @@ def write(self, filename, compress=False, compression_level=-1):
f.write('ElementType = ' + self.get_metaimage_type() + '\n')
f.write('ElementSpacing = ' + tuple_to_string(self.attributes['ElementSpacing']) + '\n')
f.write('ElementNumberOfChannels = ' + str(self.attributes['ElementNumberOfChannels']) + '\n')
f.write('Offset = ' + tuple_to_string(self.attributes['Offset']) + '\n')
if compress:
compressed_raw_data = zlib.compress(raw_data, compression_level)
f.write('CompressedData = True\n')
f.write('CompressedDataSize = ' + str(len(compressed_raw_data)) + '\n')
for key, value in self.attributes.items():
if key not in ['NDims', 'DimSize', 'ElementType', 'ElementDataFile', 'CompressedData', 'CompressedDataSize', 'ElementSpacing', 'ElementNumberOfChannels']:
if key not in ['NDims', 'DimSize', 'ElementType', 'ElementDataFile', 'CompressedData', 'CompressedDataSize', 'ElementSpacing', 'ElementNumberOfChannels', 'Offset']:
f.write(key + ' = ' + value + '\n')
f.write('ElementDataFile = ' + raw_filename + '\n')

Expand Down
153 changes: 98 additions & 55 deletions exporters/spline_segmentation_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def add_subjects_to_path(self, path, data):
target_gt_name = os.path.splitext(target_name)[0]+"_gt.mhd"

filename = image_sequence.format.replace('#', str(frame.frame_nr))
image_metadata = None
if filename.endswith('mhd'):
image_metadata = MetaImage(filename=filename)
new_filename = join(subject_subfolder, target_name)
copy_image(filename, new_filename)

Expand All @@ -89,7 +92,7 @@ def add_subjects_to_path(self, path, data):
image_pil = PIL.Image.open(new_filename)
image_size = image_pil.size
spacing = [1, 1]
self.save_segmentation(frame, image_size, join(subject_subfolder, target_gt_name), spacing)
self.save_segmentation(frame, image_size, join(subject_subfolder, target_gt_name), spacing, image_metadata)

return True, path

Expand All @@ -102,57 +105,8 @@ def get_object_segmentation(self, image_size, frame):
for label in labels:
objects = ControlPoint.objects.filter(label=label, image=frame).only('object').distinct()
for object in objects:
previous_x = None
previous_y = None
control_points = ControlPoint.objects.filter(label=label, image=frame, object=object.object).order_by('index')
max_index = len(control_points)
for i in range(max_index):
if i == 0:
first = max_index-1
else:
first = i-1
a = control_points[first]
b = control_points[i]
c = control_points[(i+1) % max_index]
d = control_points[(i+2) % max_index]
length = sqrt((b.x - c.x)*(b.x - c.x) + (b.y - c.y)*(b.y - c.y))
# Not a very elegant solution ... could try to estimate the spline length instead
# or draw straight lines between consecutive points instead
step_size = min(0.01, 1.0 / (length*2))
for t in np.arange(0, 1, step_size):
x = (2 * t * t * t - 3 * t * t + 1) * b.x + \
(1 - tension) * (t * t * t - 2.0 * t * t + t) * (c.x - a.x) + \
(-2 * t * t * t + 3 * t * t) * c.x + \
(1 - tension) * (t * t * t - t * t) * (d.x - b.x)
y = (2 * t * t * t - 3 * t * t + 1) * b.y + \
(1 - tension) * (t * t * t - 2.0 * t * t + t) * (c.y - a.y) + \
(-2 * t * t * t + 3 * t * t) * c.y + \
(1 - tension) * (t * t * t - t * t) * (d.y - b.y)

# Round and snap to borders
x = int(round(x))
x = min(image_size[1]-1, max(0, x))
y = int(round(y))
y = min(image_size[0]-1, max(0, y))

if previous_x is not None and (abs(previous_x - x) > 1 or abs(previous_y - y) > 1):
# Draw a straight line between the points
end_pos = np.array([x,y])
start_pos = np.array([previous_x,previous_y])
direction = end_pos - start_pos
segment_length = np.linalg.norm(end_pos - start_pos)
direction = direction / segment_length # Normalize
for i in np.arange(0.0, np.ceil(segment_length), 0.5):
current = start_pos + direction * (float(i)/np.ceil(segment_length))
current = np.round(current).astype(np.int32)
current[0] = min(image_size[1]-1, max(0, current[0]))
current[1] = min(image_size[0]-1, max(0, current[1]))
segmentation[current[1], current[0]] = counter

previous_x = x
previous_y = y

segmentation[y, x] = counter
self.draw_segmentation(image_size, control_points, canvas=segmentation, label=counter)

# Fill the hole
segmentation[binary_fill_holes(segmentation == counter)] = counter
Expand All @@ -161,14 +115,103 @@ def get_object_segmentation(self, image_size, frame):

return segmentation

def save_segmentation(self, frame, image_size, filename, spacing):
@staticmethod
def draw_segmentation(image_size, control_points, label: int = 1, canvas: np.ndarray = None, tension: float = 0.5):
if canvas is None:
canvas = np.zeros(image_size, dtype=np.uint8)

previous_x = None
previous_y = None

max_index = len(control_points)
for i in range(max_index):
if i == 0:
first = max_index - 1
else:
first = i - 1
a = control_points[first]
b = control_points[i]
c = control_points[(i + 1) % max_index]
d = control_points[(i + 2) % max_index]
length = sqrt((b.x - c.x) * (b.x - c.x) + (b.y - c.y) * (b.y - c.y))
# Not a very elegant solution ... could try to estimate the spline length instead
# or draw straight lines between consecutive points instead
step_size = min(0.01, 1.0 / (length * 2))
for t in np.arange(0, 1, step_size):
x = (2 * t * t * t - 3 * t * t + 1) * b.x + \
(1 - tension) * (t * t * t - 2.0 * t * t + t) * (c.x - a.x) + \
(-2 * t * t * t + 3 * t * t) * c.x + \
(1 - tension) * (t * t * t - t * t) * (d.x - b.x)
y = (2 * t * t * t - 3 * t * t + 1) * b.y + \
(1 - tension) * (t * t * t - 2.0 * t * t + t) * (c.y - a.y) + \
(-2 * t * t * t + 3 * t * t) * c.y + \
(1 - tension) * (t * t * t - t * t) * (d.y - b.y)

# Round and snap to borders
x = int(round(x))
x = min(image_size[1] - 1, max(0, x))
y = int(round(y))
y = min(image_size[0] - 1, max(0, y))

if previous_x is not None and (abs(previous_x - x) > 1 or abs(previous_y - y) > 1):
# Draw a straight line between the points
end_pos = np.array([x, y])
start_pos = np.array([previous_x, previous_y])
direction = end_pos - start_pos
segment_length = np.linalg.norm(end_pos - start_pos)
direction = direction / segment_length # Normalize
for i in np.arange(0.0, np.ceil(segment_length), 0.5):
current = start_pos + direction * (float(i) / np.ceil(segment_length))
current = np.round(current).astype(np.int32)
current[0] = min(image_size[1] - 1, max(0, current[0]))
current[1] = min(image_size[0] - 1, max(0, current[1]))
canvas[current[1], current[0]] = label

previous_x = x
previous_y = y

canvas[y, x] = label

return canvas

@staticmethod
def compute_scaling(image_size, spacing):
if len(spacing) == 2:
aspect_ratio = image_size[0] / image_size[1]
new_aspect_ratio = image_size[0] * spacing[0] / (image_size[1] * spacing[1])
scale = new_aspect_ratio / aspect_ratio
pixel_scaling = np.divide(image_size, np.multiply(image_size, scale).astype(int))
else:
raise NotImplementedError('3D segmentations not implemented yet')
return pixel_scaling

def save_segmentation(self, frame, image_size, filename, spacing, image_metadata: MetaImage = None):
image_size = [image_size[1], image_size[0]]

# Create compounded segmentation object
segmentation = self.get_object_segmentation(image_size, frame)
if np.any(spacing != 1):
print('Anisotropic image detected')
segmentation = np.zeros(image_size, dtype=np.uint8)
labels = Label.objects.filter(task=frame.image_annotation.task).order_by('id')
scaling = self.compute_scaling(image_size, spacing)
# TODO: NotImplementedError will be triggered if we are dealing with 3D data
for label, label_id in enumerate(labels):
objects = ControlPoint.objects.filter(label=label_id, image=frame).only('object').distinct()
for object in objects:
control_points = ControlPoint.objects.filter(label=label_id, image=frame, object=object.object).order_by('index')
for point in control_points:
point.x *= scaling[0]
# Update segmentation
object_segmentation = self.draw_segmentation(image_size, control_points)
object_segmentation[binary_fill_holes(object_segmentation == 1)] = 1
segmentation[object_segmentation == 1] = label + 1
else:
# Create compounded segmentation object
segmentation = self.get_object_segmentation(image_size, frame)

segmentation_mhd = MetaImage(data=segmentation)
segmentation_mhd.set_attribute('ImageQuality', frame.image_annotation.image_quality)
if image_metadata is not None:
segmentation_mhd.set_attribute('FrameType', image_metadata.get_metaimage_type())
segmentation_mhd.set_attribute('Offset', image_metadata.get_origin())
segmentation_mhd.set_spacing(spacing)
metadata = ImageMetadata.objects.filter(image=frame.image_annotation.image)
for item in metadata:
Expand Down
16 changes: 9 additions & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Django==2.2.*
h5py==2.10.*
numpy==1.19.*
Pillow==7.1.*
scipy>=1.5.0,<2.0
django-otp==0.7.*
qrcode==6.*
Django==2.2.13
django-otp==0.7.4
h5py==2.10.0
Pillow==7.1.0
pytz==2021.1
qrcode==6.1
scipy==1.5.4
six==1.15.0
sqlparse==0.4.1
Empty file added video_annotation/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions video_annotation/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.contrib import admin
from .models import *

# Register your models here.
admin.site.register(VideoAnnotation)
Loading