Skip to content
This repository has been archived by the owner on Feb 22, 2020. It is now read-only.

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
dchansen authored Sep 28, 2017
2 parents 11b8146 + 87df6fc commit d888503
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ test/assets/* filter=lfs diff=lfs merge=lfs -text
*.mhd filter=lfs diff=lfs merge=lfs -text
*.raw filter=lfs diff=lfs merge=lfs -text
*.ckpt filter=lfs diff=lfs merge=lfs -text

72 changes: 72 additions & 0 deletions interface/backend/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import dicom
import base64
from PIL import Image
from io import BytesIO
from backend.cases.models import (
Case,
Candidate,
Expand Down Expand Up @@ -66,3 +70,71 @@ def create(self, validated_data):
candidate=validated_data['candidate'],
centroid=ImageLocation.objects.create(**validated_data['centroid']),
)

class DicomMetadataSerializer(serializers.BaseSerializer):
'''
Serialize a Dicom image metadata including a base64 version of the
image in following format:
{
metadata: {
"Specific Character Set": "ISO_IR 100",
"Image Type": [
"ORIGINAL",
"SECONDARY",
"AXIAL"
],
"SOP Class UID": "1.2.840.10008.5.1.4.1.1.2",
......
},
image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQ.....fnkw3n"
}
'''

def to_representation(self, obj):
'''
Put dicom metadata into a separate dictionary
'''
dicom_dict = {}
repr(obj) # Bit hacky! But does the work to populate the elements
for dicom_value in obj.values():
if dicom_value.tag == (0x7fe0, 0x0010):
# discard pixel data
continue
if isinstance(dicom_value.value, dicom.dataset.Dataset):
dicom_dict[dicom_value.name] = self.dicom_dataset_to_dict(dicom_value.value)
else:
dicom_dict[dicom_value.name] = self._convert_value(dicom_value.value)
return {
'metadata': dicom_dict,
'image': self.dicom_to_base64(obj),
}

def dicom_to_base64(self, ds):
'''
Returning base64 encoded string for a dicom image
'''
buff_output = BytesIO()
img = Image.fromarray((ds.pixel_array)).convert('RGB')
img.save(buff_output, format='jpeg')
return 'data:image/jpg;base64,' + \
base64.b64encode(buff_output.getvalue()).decode()

def _sanitise_unicode(self, s):
return s.replace(u"\u0000", "").strip()

def _convert_value(self, v):
if isinstance(v, (list, int, float)):
converted_val = v
elif isinstance(v, str):
converted_val = self._sanitise_unicode(v)
elif isinstance(v, bytes):
converted_val = self._sanitise_unicode(v.decode('ascii', 'replace'))
elif isinstance(v, dicom.valuerep.DSfloat):
converted_val = float(v)
elif isinstance(v, dicom.valuerep.IS):
converted_val = int(v)
elif isinstance(v, dicom.valuerep.PersonName3):
converted_val = str(v)
else:
converted_val = repr(v)
return converted_val
9 changes: 9 additions & 0 deletions interface/backend/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ def test_images_available_view(self):
url = reverse('images-available')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_images_metadata_view(self):
url = reverse('images-metadata')
response = self.client.get(url, {
'dicom_location': '/images/LIDC-IDRI-0002/' \
'1.3.6.1.4.1.14519.5.2.1.6279.6001.490157381160200744295382098329/' \
'1.3.6.1.4.1.14519.5.2.1.6279.6001.619372068417051974713149104919/-80.750000.dcm'
})
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_candidates_mark(self):
candidate = CandidateFactory()
Expand Down
2 changes: 2 additions & 0 deletions interface/backend/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
NoduleViewSet,
ImageSeriesViewSet,
ImageAvailableApiView,
ImageMetadataApiView,
candidate_mark,
candidate_dismiss,
case_report,
Expand All @@ -26,6 +27,7 @@
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^images/available$', ImageAvailableApiView.as_view(), name='images-available'),
url(r'^images/metadata$', ImageMetadataApiView.as_view(), name='images-metadata'),
url(r'^candidates/(?P<candidate_id>\d+)/dismiss$', candidate_dismiss, name='candidate-dismiss'),
url(r'^candidates/(?P<candidate_id>\d+)/mark$', candidate_mark, name='candidate-mark'),
url(r'^nodules/(?P<nodule_id>\d+)/update$', nodule_update, name='nodule-update'),
Expand Down
23 changes: 21 additions & 2 deletions interface/backend/api/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import json
import mimetypes
import os

import dicom
from backend.api import serializers
from backend.cases.models import (
Case,
Candidate,
Nodule,
CaseSerializer
)
from rest_framework.views import APIView
from rest_framework.response import Response
from backend.images.models import ImageSeries
from django.conf import settings
from django.core.files.storage import FileSystemStorage
Expand All @@ -20,7 +22,6 @@
from rest_framework.decorators import renderer_classes
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView


class CaseViewSet(viewsets.ModelViewSet):
Expand All @@ -43,6 +44,24 @@ class ImageSeriesViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ImageSeriesSerializer


class ImageMetadataApiView(APIView):

def get(self, request):
'''
Get metadata of a DICOM image including the image in base64 format.
Example: .../api/images/metadata?dicom_location=FULL_PATH_TO_IMAGE
---
parameters:
- name: dicom_location
description: full location of the image
required: true
type: string
'''
path = request.GET['dicom_location']
ds = dicom.read_file(path, force=True)
return Response(serializers.DicomMetadataSerializer(ds).data)


class ImageAvailableApiView(APIView):
"""
View list of images from dataset directory
Expand Down
4 changes: 4 additions & 0 deletions interface/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ django-dotenv==1.4.1
django-environ==0.4.3

# Images
Pillow==4.2.1
pydicom==0.9.9

# Data
numpy==1.13.1
2 changes: 1 addition & 1 deletion prediction/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ scikit-image==0.13.0
SimpleITK==1.0.1
http://download.pytorch.org/whl/cu80/torch-0.2.0.post3-cp36-cp36m-manylinux1_x86_64.whl
opencv-python==3.3.0.10
pandas==0.20.3
pandas==0.20.3
2 changes: 1 addition & 1 deletion prediction/src/algorithms/classify/trained_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
An API for a trained classification model to make predictions
for if nodules are concerning or not.
"""

from src.algorithms.classify.src import gtr123_model
from src.preprocess.load_ct import load_ct, MetaData

Expand Down Expand Up @@ -64,6 +63,7 @@ def predict(dicom_path, centroids, model_path=None,
else:
preprocessed = voxel_data


model_path = model_path or "src/algorithms/classify/assets/gtr123_model.ckpt"

return gtr123_model.predict(preprocessed, centroids, model_path)
5 changes: 1 addition & 4 deletions prediction/src/algorithms/identify/trained_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@

from src.algorithms.identify.src import gtr123_model



def predict(dicom_path):
""" Predicts centroids of nodules in a DICOM image.
Expand All @@ -37,7 +35,6 @@ def predict(dicom_path):
'z': int,
'p_nodule': float}
"""

reader = sitk.ImageSeriesReader()
filenames = reader.GetGDCMSeriesFileNames(dicom_path)
if not filenames:
Expand All @@ -47,4 +44,4 @@ def predict(dicom_path):
image = reader.Execute()
result = gtr123_model.predict(image)
return result

Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ def test_classify_predict_model_load(dicom_path, model_path):
preprocess_ct=None,
preprocess_model_input=None)


assert len(predicted) == 0


def test_classify_predict_inference(dicom_path, model_path):
predicted = trained_model.predict(dicom_path,
[{'x': 50, 'y': 50, 'z': 21}],
model_path)

assert len(predicted) == 1
assert isinstance(predicted[0]['p_concerning'], float)
assert predicted[0]['p_concerning'] >= 0.
Expand Down
4 changes: 2 additions & 2 deletions prediction/src/tests/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
Provides unit tests for the API endpoints.
"""

import json
import os
from functools import partial
import json

import pytest

from flask import url_for
from src.algorithms import classify, identify, segment
from src.factory import create_app
Expand Down Expand Up @@ -172,3 +171,4 @@ def test_other_error(client):
data = get_data(r)
assert r.status_code == 500
assert "The path doesn't contain neither .mhd nor .dcm files" in data['error']

0 comments on commit d888503

Please sign in to comment.