Diff of /DigiPathAI/main_server.py [000000] .. [5bd30d]

Switch to unified view

a b/DigiPathAI/main_server.py
1
#!/usr/bin/env python
2
#
3
from collections import OrderedDict
4
from flask import Flask, abort, make_response, render_template, url_for
5
from flask import flash
6
from io import BytesIO
7
import openslide
8
from openslide import OpenSlide, OpenSlideError
9
from openslide.deepzoom import DeepZoomGenerator
10
import os
11
from optparse import OptionParser
12
from threading import Lock
13
import json
14
import threading
15
import time
16
from queue import Queue 
17
import sys
18
import glob
19
from flask import request
20
21
SLIDE_DIR = 'examples'
22
VIEWER_ONLY = True
23
SLIDE_CACHE_SIZE = 10
24
DEEPZOOM_FORMAT = 'jpeg'
25
DEEPZOOM_TILE_SIZE = 254
26
DEEPZOOM_OVERLAP = 1
27
DEEPZOOM_LIMIT_BOUNDS = True
28
DEEPZOOM_TILE_QUALITY = 75
29
30
app = Flask(__name__)
31
app.config.from_object(__name__)
32
app.config.from_envvar('DEEPZOOM_MULTISERVER_SETTINGS', silent=True)
33
34
class PILBytesIO(BytesIO):
35
    def fileno(self):
36
        '''Classic PIL doesn't understand io.UnsupportedOperation.'''
37
        raise AttributeError('Not supported')
38
39
class _SlideCache(object):
40
    def __init__(self, cache_size, dz_opts):
41
        self.cache_size = cache_size
42
        self.dz_opts = dz_opts
43
        self._lock = Lock()
44
        self._cache = OrderedDict()
45
46
    def get(self, path):
47
        with self._lock:
48
            if path in self._cache:
49
                # Move to end of LRU
50
                slide = self._cache.pop(path)
51
                self._cache[path] = slide
52
                return slide
53
54
        osr = OpenSlide(path)
55
        slide = DeepZoomGenerator(osr, **self.dz_opts)
56
        try:
57
            mpp_x = osr.properties[openslide.PROPERTY_NAME_MPP_X]
58
            mpp_y = osr.properties[openslide.PROPERTY_NAME_MPP_Y]
59
            slide.mpp = (float(mpp_x) + float(mpp_y)) / 2
60
        except (KeyError, ValueError):
61
            slide.mpp = 0
62
63
        with self._lock:
64
            if path not in self._cache:
65
                if len(self._cache) == self.cache_size:
66
                    self._cache.popitem(last=False)
67
                self._cache[path] = slide
68
        return slide
69
70
class _Directory(object):
71
    def __init__(self, basedir, relpath=''):
72
        self.name = os.path.basename(relpath)
73
        self.full_name = '.' if relpath=='' else './'+relpath
74
        self.children = []
75
        self.children_masks = []
76
        for name in sorted(os.listdir(os.path.join(basedir, relpath))):
77
            cur_relpath = os.path.join(relpath, name)
78
            cur_path = os.path.join(basedir, cur_relpath)
79
            if os.path.isdir(cur_path):
80
                cur_dir = _Directory(basedir, cur_relpath)
81
                if cur_dir.children:
82
                    self.children.append(cur_dir)
83
            elif OpenSlide.detect_format(cur_path):
84
                if not ('dgai-mask' in os.path.basename(cur_path)) and not ('dgai-uncertainty' in os.path.basename(cur_path)):
85
                    #liver-slide-1-slide.tiff -> liver-slide-1-mask.tiff
86
                    if get_mask_path(cur_path):
87
                        self.children.append(_SlideFile(cur_relpath,True))
88
                    else:
89
                        self.children.append(_SlideFile(cur_relpath,False))
90
91
class _SlideFile(object):
92
    def __init__(self, relpath, mask_present):
93
        self.name = os.path.basename(relpath)
94
        self.url_path = relpath
95
        self.mask_present = mask_present
96
97
@app.before_first_request
98
def _setup():
99
    app.basedir = os.path.abspath(app.config['SLIDE_DIR'])
100
    config_map = {
101
        'DEEPZOOM_TILE_SIZE': 'tile_size',
102
        'DEEPZOOM_OVERLAP': 'overlap',
103
        'DEEPZOOM_LIMIT_BOUNDS': 'limit_bounds',
104
    }
105
    opts = dict((v, app.config[k]) for k, v in config_map.items())
106
    app.cache = _SlideCache(app.config['SLIDE_CACHE_SIZE'], opts)
107
    app.segmentation_status = {"status":""}
108
109
def get_mask_path_basename(path):
110
    return os.path.splitext(path)[0]+'-dgai-mask'
111
112
def get_mask_path(path):
113
    '''
114
        Returns the path of the associated mask if it exists or returns False
115
        Example: 'folder/liver-cancer.svs > folder/liver-cancer-dgai-mask
116
    '''
117
    mask_path = glob.glob(get_mask_path_basename(path)+'*')
118
    if mask_path == []:
119
        return False
120
    elif len(mask_path) >1:
121
        raise ValueError("Duplicate masks found")
122
    else:
123
        return mask_path[0]
124
125
def get_uncertainty_path(path):
126
    mask_path =  '-'.join(path.split('-')[:-1]+["uncertainty"])+'.'+path.split('.')[-1]
127
    # mask_path = mask_path.replace('.svs', '.tiff')
128
    return mask_path
129
130
def _get_slide(path):
131
    path = os.path.abspath(os.path.join(app.basedir, path))
132
    if not path.startswith(app.basedir + os.path.sep):
133
        # Directory traversal
134
        abort(404)
135
    if not os.path.exists(path):
136
        abort(404)
137
    try:
138
        slide = app.cache.get(path)
139
        slide.filename = os.path.basename(path)
140
        return slide
141
    except OpenSlideError:
142
        abort(404)
143
144
@app.route('/')
145
def index():
146
    return render_template('files.html', root_dir=_Directory(app.basedir))
147
148
@app.route('/segment', methods=['POST'])
149
def segment():
150
    app.segmentation_status['tissuetype'] = request.form['tissuetype']
151
    if VIEWER_ONLY:
152
        app.segmentation_status['status'] = VIEWER_ONLY
153
    else:
154
        sys.path.append('..')
155
        from DigiPathAI.Segmentation import getSegmentation
156
        x = threading.Thread(target = run_segmentation, args = (app.segmentation_status, getSegmentation))
157
        x.start()
158
    return app.segmentation_status
159
160
161
def run_segmentation(status, getSegmentation):
162
    status['status'] = "Running"
163
    print(status)
164
    print("Starting segmentation")
165
    getSegmentation(img_path = status['slide_path'],
166
                mask_path = get_mask_path(status['slide_path']),
167
                uncertainty_path = get_uncertainty_path(status['slide_path']),
168
                status = status,
169
                mode  = status['tissuetype'])
170
    time.sleep(0.1)
171
    print("Segmentation done")
172
    status['status'] = "Done"
173
174
175
@app.route('/check_segment_status')
176
def check_segment_status():
177
    return app.segmentation_status
178
179
def get_slide_properties(slide_path):
180
    '''
181
    Calculates and returns slide properties as a dictionary
182
    '''
183
    slide_dims = OpenSlide(slide_path).dimensions
184
    properties={'Dimensions':'%d x %d pixel' %(slide_dims[1],slide_dims[0]) }
185
    slide_area = slide_dims[0]*slide_dims[1]
186
    if (slide_area/1e6) != 0:
187
        properties['Area']='%d million pixels' % int(slide_dims[1]*slide_dims[0]/1e6) 
188
    elif (slide_area/1e3) != 0:
189
        properties['Area']='%d thousand pixels' % int(slide_dims[1]*slide_dims[0]/1e4)
190
    else:
191
        properties['Area']='%d pixels' % int(slide_dims[1]*slide_dims[0])
192
    return properties
193
194
@app.route('/<path:path>')
195
def slide(path):
196
    slide= _get_slide(path)
197
    slide_url = url_for('dzi', path=path)
198
    mask_url = get_mask_path(path)
199
    uncertainty_url = get_uncertainty_path(path)
200
    if mask_url  != False:
201
        mask_url = '.'.join([slide_url.split('.')[0]+'-dgai-mask']+ slide_url.split('.')[1:])
202
    if uncertainty_url  != False:
203
        uncertainty_url = '.'.join([slide_url.split('.')[0]+'-dgai-uncertainty']+ slide_url.split('.')[1:])
204
    print(slide_url)
205
206
    path = os.path.abspath(os.path.join(app.basedir, path))
207
    app.segmentation_status['slide_path'] = path
208
    properties = get_slide_properties(path)
209
210
    return render_template('viewer.html', slide_url=slide_url,mask_url=mask_url,uncertainty_url=uncertainty_url,viewer_only=VIEWER_ONLY,properties=properties,
211
            slide_filename=slide.filename, slide_mpp=slide.mpp, root_dir=_Directory(app.basedir) )
212
213
214
@app.route('/about')
215
def about_info():
216
    return render_template('about.html')
217
218
@app.route('/<path:path>.dzi')
219
def dzi(path):
220
    slide = _get_slide(path)
221
    format = app.config['DEEPZOOM_FORMAT']
222
    resp = make_response(slide.get_dzi(format))
223
    resp.mimetype = 'application/xml'
224
    return resp
225
226
@app.route('/<path:path>_files/<int:level>/<int:col>_<int:row>.<format>')
227
def tile(path, level, col, row, format):
228
    slide = _get_slide(path)
229
    format = format.lower()
230
    if format != 'jpeg' and format != 'png':
231
        # Not supported by Deep Zoom
232
        abort(404)
233
    try:
234
        tile = slide.get_tile(level, (col, row))
235
    except ValueError:
236
        # Invalid level or coordinates
237
        abort(404)
238
    buf = PILBytesIO()
239
    tile.save(buf, format, quality=app.config['DEEPZOOM_TILE_QUALITY'])
240
    resp = make_response(buf.getvalue())
241
    resp.mimetype = 'image/%s' % format
242
    return resp
243
244
245
def main():
246
    parser = OptionParser(usage='Usage: %prog [options] [slide-directory]')
247
    parser.add_option('-s','--slide_dir',default='.',help="Directory containing the images. Defaults is current directory")
248
    parser.add_option('-B', '--ignore-bounds', dest='DEEPZOOM_LIMIT_BOUNDS',
249
                default=True, action='store_false',
250
                help='display entire scan area')
251
    parser.add_option('-c', '--config', metavar='FILE', dest='config',
252
                help='config file')
253
    parser.add_option('-d', '--debug', dest='DEBUG', action='store_true',
254
                help='run in debugging mode (insecure)')
255
    parser.add_option('-e', '--overlap', metavar='PIXELS',
256
                dest='DEEPZOOM_OVERLAP', type='int',
257
                help='overlap of adjacent tiles [1]')
258
    parser.add_option('-f', '--format', metavar='{jpeg|png}',
259
                dest='DEEPZOOM_FORMAT',
260
                help='image format for tiles [jpeg]')
261
    parser.add_option('-l', '--listen', metavar='ADDRESS', dest='host',
262
                default='127.0.0.1',
263
                help='address to listen on [127.0.0.1]')
264
    parser.add_option('-p', '--port', metavar='PORT', dest='port',
265
                type='int', default=8080,
266
                help='port to listen on [8080]')
267
    parser.add_option('-Q', '--quality', metavar='QUALITY',
268
                dest='DEEPZOOM_TILE_QUALITY', type='int',
269
                help='JPEG compression quality [75]')
270
    parser.add_option('-S', '--size', metavar='PIXELS',
271
                dest='DEEPZOOM_TILE_SIZE', type='int',
272
                help='tile size [254]')
273
    parser.add_option('--viewer-only', action='store_true',dest='viewer_only',
274
                help='disable segmentation')
275
    (opts, args) = parser.parse_args()
276
277
    global VIEWER_ONLY
278
279
    if opts.viewer_only==True:
280
        VIEWER_ONLY = True
281
    else:
282
        VIEWER_ONLY = False
283
284
    if opts.DEBUG == None:
285
        opts.DEBUG = False
286
287
    # Set slide directory
288
    app.config['SLIDE_DIR'] = opts.slide_dir
289
290
    if opts.config is not None:
291
        app.config.from_pyfile(opts.config)
292
    # Overwrite only those settings specified on the command line
293
    for k in dir(opts):
294
        if not k.startswith('_') and getattr(opts, k) is None:
295
            delattr(opts, k)
296
    app.config.from_object(opts)
297
    app.run(host=opts.host, port=opts.port,debug=opts.DEBUG, threaded=True)
298
299
if __name__ == "__main__":
300
    main()