Diff of /main.py [000000] .. [fcbb05]

Switch to unified view

a b/main.py
1
#!/usr/bin/env python3
2
3
from random import shuffle
4
import src.util as APL_UTIL
5
6
import numpy as np
7
from matplotlib import pyplot as plt
8
from matplotlib import patches
9
import cv2
10
11
import tensorflow
12
from tensorflow import keras
13
from tensorflow.keras.preprocessing.image import ImageDataGenerator
14
15
import kivy
16
17
from kivy.app import App
18
from kivy.lang import Builder
19
from kivy.core.window import Window
20
from kivy.clock import Clock
21
22
from kivy.uix.screenmanager import ScreenManager
23
from kivy.uix.screenmanager import Screen
24
from kivy.uix.popup import Popup
25
26
from kivy.uix.widget import Widget
27
from kivy.uix.button import Button
28
29
from kivy.uix.boxlayout import BoxLayout
30
from kivy.uix.gridlayout import GridLayout
31
from kivy.uix.floatlayout import FloatLayout
32
from kivy.uix.stacklayout import StackLayout
33
34
from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg as Figure
35
36
from kivy.config import Config
37
38
import os
39
40
kivy.require('1.10.0')
41
Window.clearcolor = .85, .85, .85, 1
42
43
IMG_SCALE = 64
44
45
class Chip(BoxLayout): pass
46
class ChipInput(Chip):
47
    def __init__(self, *args, **kwargs):
48
        super(ChipInput, self).__init__(*args, **kwargs)
49
        self.callback = lambda x: None
50
51
    def submit(self):
52
        self.callback(self.ids.input.text)
53
        
54
class ChipInputAdder(Chip):
55
    def __init__(self, *args, **kwargs):
56
        super(ChipInputAdder, self).__init__(*args, **kwargs)
57
        self.callback = lambda x: None
58
59
    def submit(self):
60
        self.callback(self.ids.input.text)
61
        self.ids.input.text = ''
62
63
class ChipRemovable(Chip):
64
    def __init__(self, *args, **kwargs):
65
        super(ChipRemovable, self).__init__(*args, **kwargs)
66
67
    def on_press(self):
68
        self.selected = not self.selected
69
70
    def on_touch_down(self, touch):
71
        if self.collide_point(*touch.pos):
72
            self.ids.btn_remove.on_touch_down(touch)
73
        
74
            self.on_press()
75
        
76
    def remove(self):
77
        pass
78
79
80
class Plot(Widget):
81
    def __init__(self, *args, **kwargs):
82
        super(Plot, self).__init__(*args, **kwargs)
83
84
        self.orientation = 'vertical'
85
        self.background_color = 0, 0, 0, 0
86
        self.size_hint = None, None
87
88
        self.fig = plt.figure()
89
        self.fig.subplots_adjust(bottom=0, left=0, top=1, right=1)
90
91
        self.fig.patch.set_facecolor((0, 0, 0, 0))
92
93
        self.plot = self.fig.add_subplot(111)
94
        
95
    def update(self):
96
        self.fig.canvas.draw()
97
98
class ImagePlot(BoxLayout):
99
    def __init__(self, *args, **kwargs):
100
        super(ImagePlot, self).__init__(*args, **kwargs)
101
102
        self.orientation = 'vertical'
103
        self.background_color = 0, 0, 0, 0
104
        self.size_hint = None, None
105
106
        self.fig = plt.figure()
107
        self.fig.subplots_adjust(bottom=0, left=0, top=1, right=1)
108
109
        self.fig.patch.set_facecolor((0, 0, 0, 0))
110
111
        
112
        self.img_plot = self.fig.add_subplot(111)
113
        self.img_plot.set_axis_off()
114
        self.set_image(np.zeros((1, 1, 3)))
115
116
        self.view_box = patches.Rectangle((0, 0), 0, 0, fill=False)
117
        self.view_box.set_linestyle('--')
118
        self.img_plot.add_patch(self.view_box)
119
120
        self.figure = Figure(self.fig)
121
        self.figure.pos_hint = { 'left': 0, 'bottom': 0 }
122
        self.figure.size_hint = (1, 1)
123
        self.add_widget(self.figure)
124
125
    def update_viewbox(self, x, y, w, h):
126
        self.view_box.set_xy((x, y))
127
        self.view_box.set_width(w)
128
        self.view_box.set_height(h)
129
        self.fig.canvas.draw()
130
131
    def set_image(self, img):
132
        self.source_img = img
133
        self.update_image()
134
135
    def update_image(self):
136
        self.img_plot.imshow(self.source_img, interpolation='nearest')
137
        self.fig.canvas.draw()
138
139
    def load_image(self, path):
140
        self.set_image(plt.imread(path))
141
142
class PopupFileLoader(Popup):
143
    def __init__(self, callback, *args, **kwargs):
144
        super(PopupFileLoader, self).__init__(*args, **kwargs)
145
        self.callback = callback
146
147
    def selectFile(self, file):
148
        self.file_path = file[0] if file else None
149
150
    def submitFile(self):
151
        if self.file_path is not None:
152
            self.callback(self.file_path)
153
            self.dismiss()
154
155
    def cancelFile(self):
156
        self.file_path = None
157
        self.dimiss()
158
        
159
class ImageLoader(ImagePlot):
160
    def popup_selectImage(self, callback = lambda: None):
161
        def loader(file_path):
162
            try:
163
                self.load_image(file_path)
164
                callback()
165
            except:
166
                pass
167
168
        PopupFileLoader(loader).open()
169
        
170
class SpaceStart(Screen): pass
171
172
import queue, threading
173
174
class SpaceCreateSlide(Screen):    
175
    class Webcam(object):
176
        def __init__(self, URL):
177
            self.cap = cv2.VideoCapture(URL)
178
            self.q = queue.Queue()
179
180
            self.running = threading.Event()
181
            self.running.set()
182
183
            self.thread = threading.Thread(target=self._reader)
184
            self.thread.daemon = True
185
            self.thread.start()
186
187
        def _reader(self):
188
            while self.running.is_set():
189
                ret, frame = self.cap.read()
190
191
                if not ret:
192
                    break
193
                if not self.q.empty():
194
                    try:
195
                        self.q.get_nowait()
196
                    except queue.Empty:
197
                        pass
198
199
                self.q.put(frame)
200
            
201
        def read(self):
202
            return self.q.get()
203
204
        def terminate(self):
205
            self.running.clear()
206
            self.thread.join()
207
208
    def __init__(self, *args, **kwargs):
209
        super(SpaceCreateSlide, self).__init__(*args, **kwargs)
210
       
211
        self.ip = ''
212
        self.web_cam_on = False
213
        self.cam = None
214
215
    def set_ip(self, text):
216
        self.ip = text
217
        print(self.ip)
218
219
    def toggle_webcam(self):
220
        self.web_cam_on = not self.web_cam_on
221
222
        if self.web_cam_on:
223
            URL = f"http://{self.ip}:8080/video"
224
225
            if self.ip:
226
                print("OPENING URL", URL)
227
                
228
                def draw_capture(t):
229
                    frame = self.cam.read()
230
                    if frame is not None:
231
                        self.frame = frame
232
                        self.ids.plot.set_image(self.frame)
233
                    print(t)
234
                    return self.web_cam_on
235
236
                self.cam = SpaceCreateSlide.Webcam(URL)
237
                self.event = Clock.schedule_interval(draw_capture, 2)
238
239
            self.ids.btn_start.text = "Stop Webcam"
240
        else:
241
            if self.cam is not None:
242
                self.event.cancel()
243
                self.cam.terminate()
244
                self.cam = None
245
246
            self.ids.btn_start.text = "Start Webcam"
247
248
    def capture(self):
249
        cv2.imwrite('samples/web.png', self.frame)
250
251
class APL_Database:
252
    path = os.path.join(APL_UTIL.current_dir, 'database')
253
    samples_path = os.path.join(path, 'samples')
254
    filters_path = os.path.join(path, 'filters')
255
256
    for i in [path, samples_path, filters_path]:
257
        if not os.path.exists(i):
258
            os.makedirs(i)
259
260
    ID = 0
261
262
    @staticmethod
263
    def saveImage(img, tags):
264
        for i in tags:
265
            tag_path = os.path.join(APL_Database.samples_path, f'{i}')
266
            file_path = os.path.join(tag_path, f'subsample-{APL_Database.ID}.png')
267
            
268
            try:
269
                if not os.path.exists(tag_path):
270
                    os.makedirs(tag_path)
271
                
272
                cv2.imwrite(file_path, img)
273
            except Exception as ex:
274
                print(ex)
275
276
        APL_Database.ID += 1
277
278
    @staticmethod
279
    def loadTagData(tag, n_begin = 0, N_total = 1000):
280
        print(f"Loading [{tag}] [", end='')
281
        tag_path = os.path.join(APL_Database.samples_path, tag)
282
        
283
        imgs = []
284
        
285
        n = 0
286
        for path, _, file_names in os.walk(tag_path):
287
            for file in file_names:    
288
                if n_begin < n:
289
                    if n % 100 == 0:
290
                        print(n, end=':')
291
                
292
                    try:
293
                        imgs.append(plt.imread(os.path.join(path, file)))
294
                    except:
295
                        pass
296
                if n_begin + N_total <= n:
297
                    break
298
299
                n += 1
300
        print(']')
301
        return imgs
302
303
    @staticmethod
304
    def getAllTags():
305
        tags = []
306
        for _, dir_names, _ in os.walk(APL_Database.samples_path):
307
            for name in dir_names:
308
                name = name[:-3]
309
                if name and name not in tags:
310
                    tags.append(name)
311
        return tags
312
313
class Filter(object):
314
    POSITIVE = 0
315
    NEGATIVE = 1
316
317
    def preprocess(self, img):
318
        _img = cv2.resize(img, (self.scale, self.scale))
319
320
        if np.amax(_img) > 1:
321
            return _img / 255
322
        return _img[:,:,:3]
323
324
325
    def __init__(self, tag=''):
326
        self.scale = 64
327
328
        self.tag = tag
329
        self.path = os.path.join(APL_Database.filters_path, self.tag)
330
331
        self.key = { Filter.POSITIVE: 'positive', Filter.NEGATIVE: 'negative'}
332
333
        self.model = self.MakeV1Model()
334
335
        self.model.compile(optimizer='adam',
336
            loss='sparse_categorical_crossentropy', metrics=['accuracy'])
337
338
        self.data_queue = queue.Queue()
339
340
    def MakeV1Model(self):
341
        return keras.Sequential([
342
            keras.layers.Conv2D(16, (3, 3), activation='relu',
343
                input_shape=(self.scale, self.scale, 3)),
344
            keras.layers.MaxPool2D((2, 2)),
345
            
346
            keras.layers.Conv2D(16, (3, 3), activation='relu'),
347
            keras.layers.MaxPool2D((2, 2)),
348
349
            keras.layers.Conv2D(16, (3, 3), activation='relu'),
350
            keras.layers.MaxPool2D((2, 2)),
351
352
            keras.layers.Flatten(),
353
            keras.layers.Dense(64, activation='relu'),
354
            
355
            keras.layers.Dense(2, activation='softmax')
356
        ])
357
    def MakeV2Model(self):
358
        return keras.Sequential([
359
            keras.layers.Conv2D(16, (3, 3), activation='relu',
360
                input_shape=(self.scale, self.scale, 3)),
361
            keras.layers.MaxPool2D((2, 2)),
362
            
363
            keras.layers.Conv2D(16, (3, 3), activation='relu'),
364
            keras.layers.MaxPool2D((2, 2)),
365
366
            keras.layers.Conv2D(16, (3, 3), activation='relu'),
367
            keras.layers.MaxPool2D((2, 2)),
368
369
            keras.layers.Conv2D(16, (3, 3), activation='relu'),
370
            keras.layers.MaxPool2D((2, 2)),
371
372
            keras.layers.Flatten(),
373
            keras.layers.Dense(128, activation='relu'),
374
            keras.layers.Dense(32, activation='relu'),
375
            
376
            keras.layers.Dense(2, activation='softmax')
377
        ])
378
        
379
    def get_batch(self, n, N):
380
        pos_img = APL_Database.loadTagData(self.tag + '+ve', n, N)
381
        neg_img = APL_Database.loadTagData(self.tag + '-ve', n, N)
382
383
        data = []
384
385
        N = min(len(pos_img), len(neg_img))
386
        for i in range(N):
387
            data.append((self.preprocess(pos_img[i]), Filter.POSITIVE))
388
            data.append((self.preprocess(neg_img[i]), Filter.NEGATIVE))
389
390
        if data:
391
            return (list(t) for t in zip(*data))
392
        else:
393
            return ([], [])
394
395
    def train_model_daemon(self, plot = None):
396
        print('=' * 10, 'Loading', 10 * '=')
397
398
        N = 20000
399
        epochs = 1
400
        batch_size = 200
401
402
        for i in range(0, N - batch_size, batch_size):
403
            imgs, labels = self.get_batch(i, batch_size)
404
405
            if imgs:
406
                fixed_point = round(len(imgs) * 0.9)
407
408
                train_imgs = np.array(imgs[:fixed_point])
409
                train_labels = np.array(labels[:fixed_point])
410
411
                test_imgs = np.array(imgs[fixed_point:])
412
                test_labels = np.array(labels[fixed_point:])
413
414
                img = train_imgs[0]
415
                print('=' * 10, 'DataFormat', 10 * '=')
416
                print(f" - Train Imgs[{train_imgs.shape}] Label[{train_labels.shape}]")
417
                print(f" - Test Imgs[{test_imgs.shape}] Label[{test_labels.shape}]")
418
                print(f" - Image[{img.shape}] min {np.amin(img)} max {np.amax(img)}")
419
420
                train_gen = ImageDataGenerator(
421
                    samplewise_std_normalization = True,
422
                    brightness_range=(.0, .5),
423
                    channel_shift_range=.3,
424
                    horizontal_flip=True,
425
                    vertical_flip=True
426
                )
427
                train_gen.fit(train_imgs)
428
429
                test_gen = ImageDataGenerator(
430
                    samplewise_std_normalization = True,
431
                    brightness_range=(.0, .5),
432
                    channel_shift_range=.3,
433
                    horizontal_flip=True,
434
                    vertical_flip=True    
435
                )
436
                test_gen.fit(test_imgs)
437
                
438
                self.model.fit_generator(
439
                    train_gen.flow(train_imgs, train_labels), 
440
                    steps_per_epoch=len(train_imgs),
441
                    epochs=epochs,
442
                    validation_data=test_gen.flow(test_imgs, test_labels),
443
                    validation_steps=20,
444
                )
445
446
                #self.model.fit(train_imgs, train_labels, epochs=30)
447
                #loss, acc = self.model.evaluate(test_imgs, test_labels)
448
            else:
449
                break
450
451
    def train_model_multi(self):
452
        self.queue = queue.Queue()
453
        self.train_model_daemon()
454
455
    def train_model(self):
456
        self.queue = queue.Queue()
457
        self.train_model_daemon()
458
459
    def evaluate(self):
460
        print("=" * 10, "Evaluate", "=" * 10)
461
462
        imgs, labels = self.get_batch(0, 50)
463
        self.model.evaluate(np.array(imgs), np.array(labels), verbose=2)
464
465
    def save(self):
466
        if not os.path.exists(self.path):
467
            os.makedirs(self.path)
468
469
        self.model.save_weights(os.path.join(self.path, 'state.ckpt'))
470
        print(f"Saved {self.tag} filter")
471
        return self
472
473
    def load(self, path):
474
        latest = tensorflow.train.latest_checkpoint(path)
475
        print(latest)
476
477
        self.model.load_weights(os.path.join(path, 'state.ckpt'))
478
        self.evaluate()
479
        return self
480
481
482
    def train(self, plot = None):
483
        #Clock.schedule_once(lambda t: self.train_model(plot), 1)
484
        Clock.schedule_once(lambda t: self.train_model(), 1)
485
486
    def loadData(self):
487
        self.imgs = APL_Database.loadTagData(self.tag + '+ve')
488
        self.neg_imgs = APL_Database.loadTagData(self.tag + '-ve')
489
        return self
490
491
    def predict(self, img):
492
        return self.model.predict(np.array([self.preprocess(img)]))[0]
493
494
    @staticmethod
495
    def loadAllFilters():
496
        filters = []
497
498
        for path, sub_dir, _ in os.walk(APL_Database.filters_path):
499
            for tag_name in sub_dir:
500
                folder_path = os.path.join(path, tag_name)
501
                filters.append(Filter(tag_name).load(folder_path))
502
503
        return filters
504
505
class SpaceAnalyze(Screen): 
506
    class FilterApply(StackLayout):
507
        class Analytics(object):
508
            def __init__(self):
509
                self.grid_count = 0
510
511
                self.positives = 0
512
                self.negatives = 0
513
                self.mixed = 0
514
515
            def add_info(self, n_pos, n_neg, n_mixed):
516
                self.positives += n_pos
517
                self.negatives += n_neg
518
                self.mixed += n_mixed
519
                self.grid_count += 1
520
521
            def compile_report(self):
522
                report = f"matches    => {self.positives}\n"
523
                report += f"negatives  => {self.negatives}\n"
524
                report += f"mixed      => {self.mixed}\n"
525
                report += f"grid cells => {self.grid_count}\n"
526
                return report
527
528
        def __init__(self, filt, root_parent, *args, **kwargs):
529
            super(SpaceAnalyze.FilterApply, self).__init__(*args, **kwargs)
530
            self.filter = filt
531
            self.text = filt.tag
532
533
            self.alert = False
534
            self.interrupted = False
535
536
            self.root_parent = root_parent
537
538
            self.data = SpaceAnalyze.FilterApply.Analytics()
539
540
        def sample_report(self, img):
541
            predict = self.filter.predict(img)
542
            percent_predict = 100 * 2 * (predict - 0.5)
543
544
            if percent_predict[Filter.POSITIVE] > 50:
545
                report  = f"{self.filter.tag} +Positive {percent_predict[Filter.POSITIVE]:2.2f}%\n"
546
            
547
                self.data.add_info(1, 0, 0)
548
                self.interrupt()
549
                self.root_parent.ids.sub_sample.set_image(img)    
550
                return report, True
551
            else:
552
                report  = f"{self.filter.tag} -Negative {-percent_predict[Filter.NEGATIVE]:2.2f}%\n"
553
            
554
                self.data.add_info(0, 1, 0)
555
556
            return report, False
557
558
        def interrupt(self):
559
            self.ids.btn_train.text = 'Analyze [Next Cell]'
560
            self.interrupted = True
561
            self.root_parent.scan_interrupt()
562
563
        def analysis_callback(self):
564
            self.ids.btn_train.text = 'Analyzing ...'
565
            self.root_parent.ids.sub_sample.set_image(np.zeros((10, 10)))    
566
            
567
            if self.interrupted:
568
                self.root_parent.scan_continue()
569
            else:
570
                self.root_parent.scan_begin(self)
571
572
        def complete(self):
573
            self.root_parent.scan_reset()
574
            self.ids.btn_train.text = 'Analyze [DONE]'           
575
            self.root_parent.set_report(self.data.compile_report())
576
577
        def reset(self):
578
            self.root_parent.scan_reset()
579
            self.ids.btn_train.text = 'Analyze'
580
581
            self.data = SpaceAnalyze.FilterApply.Analytics()
582
            self.root_parent.scan_reset()
583
584
    def __init__(self, *args, **kwargs):
585
        super(SpaceAnalyze, self).__init__(**kwargs)
586
        self.loadFilters()
587
        self.scan_event = None
588
        self.scan_iter = None
589
        self.interrupted = False
590
591
    def loadFilters(self):
592
        self.ids.filter_list.clear_widgets()
593
594
        for i in Filter.loadAllFilters():
595
            widget = SpaceAnalyze.FilterApply(i, self)
596
            self.ids.filter_list.add_widget(widget)
597
598
    def contrast_img(self, img):
599
        max_val = np.amax(img)
600
        if max_val != 0:
601
            img = img.astype(float) * 255.0 / max_val
602
        
603
        img = img.astype(np.uint8)
604
        img = cv2.fastNlMeansDenoisingColored(img, None, 2, 10)
605
606
        img = img.astype('uint8')
607
608
        return img
609
610
    def find_scale(self, img):
611
        print("FINDING SCALE")
612
        
613
        w, h = img.shape[0:2]
614
615
        params = cv2.SimpleBlobDetector_Params()
616
        params.minThreshold = 50
617
        params.maxThreshold = 220
618
619
        params.filterByArea = True
620
        params.minArea = 200
621
        params.maxArea = w * h // 4
622
623
        params.filterByCircularity = True
624
        params.minCircularity = 0.2
625
626
        params.filterByConvexity = True
627
        params.minConvexity = 0.05
628
629
        params.filterByInertia = True
630
        params.minInertiaRatio = 0.01
631
632
        detector = cv2.SimpleBlobDetector_create(params)
633
        keypoints = detector.detect(img)
634
635
        diam = []
636
        for i in keypoints:
637
            diam.append(i.size)
638
639
        scl = int(np.average(diam)) if diam else min(img.shape[0:2])
640
        print("SCALE", scl)
641
642
        return scl
643
644
    def gridsplit_img(self, img, scale):
645
        print("GRIDSPLITTING")
646
        I, J = img.shape[:2]
647
        step = scale
648
        scan_list = []
649
650
        for i in range(0, I - scale, step):
651
            for j in range(0, J - scale, step):
652
                scan_list.append((i, j))
653
        for i in range(0, I - self.scale, step):
654
            scan_list.append((i, J - scale))
655
        for j in range(0, J - scale, step):
656
            scan_list.append((I - scale, j))
657
        
658
        scan_list.append((I - scale, J - scale))
659
        
660
        print("DONE")
661
        return scan_list, iter(scan_list)
662
663
    def scan_interrupt(self):
664
        self.interrupted = True    
665
666
    def scan_continue(self):
667
        if self.interrupted and self.scan_iter is not None:
668
            self.interrupted = False
669
            self.scan_event = Clock.schedule_interval(lambda t: self.scan_iterate(), 0)
670
671
    def scan_reset(self):
672
        self.set_report('REPORT')
673
        self.scan_interrupt()
674
        self.scan_event.cancel()
675
        self.scan_iter = None
676
        self.scan_list = []
677
678
    def set_report(self, text):
679
        self.report = text
680
        self.ids.info.text = text
681
682
    def scan_iterate(self):
683
        try:
684
            i, j = next(self.scan_iter)
685
        except StopIteration:
686
            self.current_filter.complete()
687
            self.scan_iter = None
688
            self.interrupted = True
689
        else:
690
            self.count += 1
691
692
            sub_sample = self.img[i:(i + self.scale), j:(j + self.scale)]
693
            self.ids.slide.update_viewbox(j, i, self.scale, self.scale)
694
            
695
            sub_report, alert = self.current_filter.sample_report(sub_sample)
696
697
            report = "-- ALERT --\n" if alert else "-- REPORT --\n"
698
            report += sub_report
699
700
            report += f"Scale     = {self.scale}\n" 
701
            report += f"Grid Cell - {self.count} of {len(self.scan_list)}\n"
702
            report += f"Position  - {(i, j)} in {self.img.shape[0:2]}\n"
703
            
704
            self.set_report(report)
705
                
706
            if alert:
707
                self.interrupted = True
708
            
709
        return not self.interrupted
710
711
    def scan_begin(self, filt):
712
        if self.scan_iter is None:
713
            self.count = 0
714
715
            self.current_filter = filt
716
            self.img = self.ids.slide.source_img
717
            self.scale = self.find_scale(self.contrast_img(self.img))
718
            self.scan_list, self.scan_iter = self.gridsplit_img(self.img, self.scale)
719
        
720
            self.interrupted = True
721
            self.scan_continue()
722
723
class FilterTrain(StackLayout):
724
    def __init__(self, filt, plot, *args, **kwargs):
725
        super(FilterTrain, self).__init__(*args, **kwargs)
726
        self.filter = filt
727
        self.text = filt.tag
728
        self.plot = plot
729
730
    def train(self):
731
        self.filter.train(self.plot)
732
733
    def save(self):
734
        self.filter.save()
735
        self.ids.btn_save.text = 'Saved'
736
737
class SpaceTrain(Screen):
738
    def load_filters(self):
739
        self.ids.filter_editor.clear_widgets()
740
    
741
        for tag in APL_Database.getAllTags():
742
            self.ids.filter_editor.add_widget(FilterTrain(Filter(tag), self.ids.plot))
743
744
class SpaceCategorize(Screen):
745
    def __init__(self, *args, **kwargs):
746
        super(SpaceCategorize, self).__init__(*args, **kwargs)
747
        self.scale = IMG_SCALE
748
749
        self.i, self.j = 0, 0
750
751
    def load_slide(self):
752
        self.ids.slide.popup_selectImage(self.next_sample)
753
754
    def next_sample(self):
755
        try:
756
            slide = self.ids.slide.source_img
757
758
            I, J = np.size(slide, 0), np.size(slide, 1)
759
760
            subsample = slide[self.i:(self.i + self.scale ), self.j:(self.j + self.scale)]
761
762
            self.ids.sub_sample.set_image(subsample)
763
            self.ids.slide.update_viewbox(self.j, self.i, self.scale, self.scale)
764
        
765
            if self.i < I - 2 * self.scale:
766
                self.i += self.scale // 2
767
            else:
768
                self.i = 0
769
                
770
                if self.j < J - 2 * self.scale:
771
                    self.j += self.scale // 2
772
                else:
773
                    self.j = 0
774
                
775
        except Exception as ex:
776
            print(ex)
777
778
    def save_tags(self):
779
        img = self.ids.sub_sample.source_img
780
781
        tags = []
782
        for i in self.ids.tags.children:
783
            if isinstance(i, ChipRemovable):
784
                if i.selected:
785
                    tags.append(i.text + '+ve')
786
                else:
787
                    tags.append(i.text + '-ve')
788
789
        APL_Database.saveImage(img, tags)
790
791
    def add_tag(self, tag):
792
        if tag:
793
            chip = ChipRemovable()
794
            chip = ChipRemovable()
795
796
            chip.text = tag
797
            chip.remove = lambda: self.ids.tags.remove_widget(chip)
798
            
799
            self.ids.tags.add_widget(chip)
800
801
class SpaceInterfaceOverview(Screen): pass
802
class SpaceCredits(Screen): pass
803
804
class WorkSpace(ScreenManager):
805
    def __init__(self, **kwargs):
806
        super(WorkSpace, self).__init__(**kwargs)
807
808
        self.add_widget(SpaceStart(name = 'screen_start'))
809
        self.add_widget(SpaceAnalyze(name = 'screen_analyze'))
810
        self.add_widget(SpaceCreateSlide(name = 'screen_createslide'))
811
        self.add_widget(SpaceTrain(name = 'screen_train'))
812
        self.add_widget(SpaceCategorize(name = 'screen_categorize'))
813
        self.add_widget(SpaceCredits(name = 'screen_credits'))
814
815
class MainWindow(BoxLayout): pass
816
class Application(App): 
817
    def build(self):
818
        self.title = 'MicroLab - DeepStain'
819
        return MainWindow()
820
821
Builder.load_file('src/style.kv')
822
823
if __name__ == '__main__':
824
    try:
825
        Application().run()
826
    except Exception as ex:
827
        print("Error:", ex)