-
Notifications
You must be signed in to change notification settings - Fork 0
/
ruler.py
1504 lines (1183 loc) · 53.5 KB
/
ruler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import xml.etree.cElementTree as ETree
from math import tan, radians
from os.path import join, isfile
from warnings import warn
from math import sin, cos, pi, sqrt
import sys
import traceback
import numpy as np
from skimage.exposure import equalize_adapthist
from skimage.feature import canny
from skimage.filters import threshold_otsu
from skimage.io import imread, imshow, show
from skimage.measure import perimeter
from skimage.morphology import binary_erosion, binary_dilation, remove_small_objects, binary_closing
from skimage.restoration import denoise_tv_chambolle
from skimage.transform import probabilistic_hough_line
from scale_reader import dist_form, line_to_angle, line_to_slope
"""
ruler.py: Provides a class to measure and temporarily store leaves and their
measurements.
Uses many of the functions from scale_reader.py, but those functions are reorganized
into a class that is better suited for batch operations.
Hopefully the the way this is designed is that it will reduce memory usage by
constantly refreshing the class's variables with each new leaf image to be measured.
"""
__author__ = "Patrick Thomas"
__credits__ = ["Patrick Thomas", "Rick Fisher"]
__version__ = "1.0.0"
__date__ = "7/8/16"
__maintainer__ = "Patrick Thomas"
__email__ = "pthomas@mail.swvgs.us"
__status__ = "Development"
DEGREES_MARGIN = 15.0 # margin of degrees to determine what is a similar hough line
# CROP_MARGIN = -5 # number of pixels to add to prevent cutting the actual image
CROP_MARGIN = 0
ADAPTHIST_CLIP_LIMIT = 0.03
DENOISE_WEIGHT = 0.2
CANNY_SIGMA = 2.5
HOUGH_PARAMS = (10, 50, 10)
MIN_TOTAL_LINES = 18
MIN_OTHER_LINES = 6
FLAT_LINE_DEGREES = 15.0 # slope range used to determine flat lines (essentially +- #)
MIDRIB_MARGIN_PERCENT = 0.05
CONTOUR_LEVELS = 3
# default scale
DEFAULT_SCALE = imread('/home/patrick/PycharmProjects/SciFair-Y2/input-images/default_scale.png', as_grey=True)
class Ruler:
"""
Provides a class to temporarily store and measure leaves easily.
New version of the scale_reader functions (essentially all scale_reader.py functions
in a class.
"""
def __init__(self):
"""
Initializes all variables to none.
Designed to be used repeatedly for many leaves.
"""
# the 'cwd' of the leaf
self.current_path = None
# raw image
self.img = None
# separated leaf and scale, in grayscale
self.scale = None
self.leaf = None
# Ostu value
self.otsu = None
# binary separated images of leaf and scale
self.scale_bin = None
self.leaf_bin = None
# scale of leaf based off of visual scale
self.vertical_cm_scale = None
self.horizontal_cm_scale = None
# vein specific measurements
'''
self.vein_measure contains the following:
{
'canny edges': vein_edges,
'hough above': above,
'hough below': below,
'hough center': midrib_lines,
'midvein': center_y,
'center range': midrib_range,
'midrib line': midrib_lin_approx,
}
'''
self.vein_measure = None
self.length = None
self.midrib_func = None
self.endpoints = None
self.area = None
# perimeter
self.perimeter_v = None
self.perimeter_h = None
self.perimeter_p = None
# surface variability
self.surf_var_mean = None
self.surf_var_median = None
# contour and angle data
self.contours_pos = None
self.contours_size = None
self.contours_angles = None
# dict o data
self.data_dict = None
def load_new_image(self, path, no_scale=False):
"""
Loads a leaf image and measures the features of the leaf.
Also updates the varaibles of the ruler class to reflect the currently measured
leaf.
:param no_scale: whether or not to look of a scale in the image provided. useful for year 1 leaves.
:param path: path of a leaf image
:return: None
"""
# reinit all values to None
self.__init__()
# set the cwd to the leaf's own path
self.current_path = path
# load the leaf from the drive as a grayscale image
# also slightly crop the image to remove any 'framing' of the picture
self.img = imread(path, as_grey=True)[5:-5, 5:-5]
# obtain an Otsu's value
self.otsu = threshold_otsu(self.img)
# if scale (default)
if not no_scale:
# split the image of the leaf automatically
self.scale, self.leaf = self.split_leaf_image()
# reduce whitespace around leaf and scale
self.scale = self.auto_crop(self.scale)
self.leaf = self.auto_crop(self.leaf)
# reduce both parts to binary and remove artifacts from image
self.scale_bin = np.bool_(self.scale <= self.otsu)
self.leaf_bin = -remove_small_objects(np.bool_(self.leaf >= self.otsu))
# no scale mode for legacy leaf pictures
elif no_scale:
# split the image of the leaf automatically
self.scale = DEFAULT_SCALE
self.leaf = self.img
# only crop the leaf
self.leaf = self.auto_crop(self.leaf)
# reduce both parts to binary and remove artifacts from image
self.scale_bin = self.scale.astype(bool)
self.leaf_bin = -remove_small_objects(np.bool_(self.leaf >= self.otsu))
# measure the scale from the binary scale image
self.vertical_cm_scale, self.horizontal_cm_scale = self.get_scale()
# measure vein data
self.vein_measure = self.measure_veins()
# measure length and perimeter
self.length = self.measure_length()
self.perimeter_p, self.perimeter_h, self.perimeter_v = self.measure_perimeter()
# measure area directly
self.area = np.sum(self.leaf_bin) / (self.vertical_cm_scale * self.horizontal_cm_scale)
# surface variability
self.surf_var_mean, self.surf_var_median = self.measure_surface_variability()
# contours
self.contours_pos, self.contours_size, self.contours_angles = self.measure_contours(levels=CONTOUR_LEVELS)
# for containing all values for xml saving
self.data_dict = dict(path=str(self.current_path),
v_cm=str(self.vertical_cm_scale),
h_cm=str(self.horizontal_cm_scale),
otsu=str(self.otsu),
p=str(self.perimeter_p),
length=str(self.length),
p_v=str(self.perimeter_h),
p_h=str(self.perimeter_v),
area=str(self.area),
sf_v_mean=str(self.surf_var_mean),
sf_v_median=str(self.surf_var_median),
array_files='unknown')
def __str__(self):
"""
Returns a useful readout of the current leaf's measurements.
:return: string of measurements
"""
string = """
MEASUREMENTS OF CURRENT LEAF
PATH {0}
CM SCALE VERTICAL {1}
HORIZONTAL {2}
OTSU {3}
PERIMETER CENTIMETERS {4}
LENGTH CENTIMETERS {5}
ARRAY FILES PATH {6}""".format(
self.data_dict['path'],
self.data_dict['v_cm'],
self.data_dict['h_cm'],
self.data_dict['otsu'],
self.data_dict['p'],
self.data_dict['length'],
self.data_dict['array_files'],
)
return string
def generate_data_dict(self, bin_nom, save_data_path='save-data'):
"""
Generates a dictionary of data that contains the most useful data from the leaf.
:param bin_nom: the name of the leaf (str)
:param save_data_path: the save path of the leaf (str)
:return: dict of data
"""
# get the file's name
img_filename = self.current_path.split('/')[-1] # last part of dir. split by slashes
# generate file name based on img and species
array_save_data_filename = "{0} - {1}".format(bin_nom, img_filename.split('.')[0])
array_save_data_path = join(save_data_path, array_save_data_filename)
data = {
# data that isn't all that important to the ANN
'path': str(self.current_path),
'v_cm': str(self.vertical_cm_scale),
'h_cm': str(self.horizontal_cm_scale),
'otsu': str(self.otsu),
# simple measurements of a leaf (one value)
'p': str(self.perimeter_p / ((self.perimeter_h + self.perimeter_v) / 2)),
'length': str(self.length),
'area': str(self.area),
'sf_v_mean': str(self.surf_var_mean),
'sf_v_median': str(self.surf_var_median),
# vein measurements
'vein_angle_above': str(self.vein_measure['angle above']),
'vein_angle_below': str(self.vein_measure['angle below']),
'vein_length': str(self.vein_measure['vein length']),
# save location
'array_files': array_save_data_path + '.npz'
}
return data
# save the currently loaded leaf, appended to the current .xml file
def save_data(self, bin_nom, save_data_path='save-data', xml_filename='leaf-data.xml'):
"""
Save the currently loaded leaf to the .xml file given by fname. The leaf is saved
under the species bin_nom.
:param bin_nom: name of leave species
:param save_data_path: path of save_data
:param xml_filename: name of the .xml file
:return: None
"""
# create new fname if fname doesn't exist on disk
if not isfile(join(save_data_path, xml_filename)):
# create preliminary 'data' tree
data = ETree.Element('data')
# old code to create a testspecies
# species = ETree.SubElement(data, 'species')
# ETree.SubElement(species, 'g').text = "testgenus"
# ETree.SubElement(species, 's').text = "testspecies"
# make a xml tree
tree = ETree.ElementTree(data)
# write tree to disk
tree.write(join(save_data_path, "leaf-data.xml"))
# load tree, should exist given previous if-statement
tree = ETree.parse(join(save_data_path, xml_filename))
# get the root of the file
root = tree.getroot()
# initalize values
child = None # tree of matching species
species_found = False # whether species exists in xml file or not
# for all species listed under <data>
for species in root.findall('species'):
# get the bin. nom. of the species
g = species.find('g')
s = species.find('s')
# if the species' actually exists (and is not empty)
if g is not None and s is not None:
# and if the name of the species in the file matches the current leaf's bin_nom
if '{0} {1}'.format(g.text, s.text) == bin_nom:
# matching species is found
species_found = True
child = species
break
# Make a new species if the species is not in the XML file
if not species_found:
# make own child
child = ETree.SubElement(root, 'species')
# split bin_nom to create species with specified data
g_str, s_str = bin_nom.split()
# add the text to the species (child tree)
g = ETree.SubElement(child, 'g')
s = ETree.SubElement(child, 's')
g.text = g_str
s.text = s_str
# get the file's name
img_filename = self.current_path.split('/')[-1] # last part of dir. split by slashes
# generate file name based on img and species
array_save_data_filename = "{0} - {1}".format(bin_nom, img_filename.split('.')[0])
array_save_data_path = join(save_data_path, array_save_data_filename)
# # determine if specific leaf is already in xml file (CURRENTLY UNNEEDED)
# img_found = False
# find all specific leaves in species category
for l in child.findall('leaf'):
# if a saved leaf's img name matches this leaf's img name
if l.attrib['name'] == img_filename:
# warn that this leaf already exists
warn_msg = "Leaf '{0}' already exists in {1}".format(img_filename, xml_filename)
warn(warn_msg, Warning)
# # say that image has been found
# img_found = True
# remove leaf's entry
child.remove(l)
break
# create new subelement of species by the leaf
leaf = ETree.SubElement(child, 'leaf', attrib={'name': img_filename})
# Write data to leaf's entry
self.data_dict = self.generate_data_dict(bin_nom, save_data_path=save_data_path)
# # more for organization
# arrays = [
# self.img,
# self.scale,
# self.leaf,
# self.scale_bin,
# self.leaf_bin,
# self.vein_measure,
# self.midrib_func,
# self.endpoints,
# ]
# write all xml compatible values in the dict as well as paths to arrays
for attribute in self.data_dict.keys():
# set all of the leaf's attributes in the xml tree to what they are supposed to be
ETree.SubElement(leaf, attribute).text = self.data_dict[attribute]
# save xml to file
tree.write(join(save_data_path, xml_filename))
# save all arrays (data to large to save in xml file and uncommonly used) into compressed .npz file
# remove the midrib linear approximation since it cannot be saved
self.vein_measure.pop('midrib lin approx', None)
# save arrays as compressed numpy format
np.savez_compressed(array_save_data_path,
img=self.img,
scale=self.scale,
leaf=self.leaf,
scale_bin=self.scale_bin,
leaf_bin=self.leaf_bin,
veins=self.vein_measure,
midrib=self.midrib_func,
endpoints=self.endpoints,
contour_pos=self.contours_pos,
contour_size=self.contours_size,
contour_angles=self.contours_angles,
)
return None
'''
def load_from_xml(self):
"""
Load all values of a given leaf from the xml file (and corresponding array file).
:return: None
"""
return None
'''
# chapter 1: preparing the images to be measured
# step 0: basically initializing the images to be measured
def split_leaf_image(self):
"""
Splits self.img by the whitespace between the scale and the leaf itself.
Separates the scale and the leaf.
:return: scale, leaf
"""
# reduces image to binary form based on Otsu value
binary = self.img <= self.otsu
# remove all small artifacts on the image (real image of this method not completely known
binary_clean = remove_small_objects(binary,
min_size=64,
connectivity=1,
in_place=False).astype(int)
# 'flatten' the image to a list of the sum of the columns
# useful for detecting presence of leaf and scale on img
# should be 0 when there is no leaf nor scale
flat = []
for column in binary_clean.T:
flat.append(sum(column))
# iterate over flat and look for 0 and non-zero values based on
# conditions of already found features
# boolean values to mark beginning and end of searches for certain features
scale_found = False
space_found = False
# actual range of mid section
mid_start = None
mid_end = None
# iterate through image
for count, c_sum in enumerate(flat):
# catch when scale not found and something in image
if not scale_found and c_sum > 0:
scale_found = True
# then catch when scale found and
elif scale_found:
# space between scale and leaf not found
if c_sum == 0 and scale_found and not space_found:
space_found = True
mid_start = count
# space is found and nothing in image (end of leaf)
elif c_sum > 0 and space_found:
mid_end = count - 1
break
# split the image between the bounds of the middle space
center_split = int((mid_start + mid_end) / 2)
# split image based on center value
scale = self.img[:, 0:center_split]
leaf = self.img[:, center_split:]
return scale, leaf
# automatically closely crop the image
def auto_crop(self, image):
"""
Automatically crops the leaf to within CROP_MARGIN of the closest pixels of
the thresholded image by the thresholded image.
:param image: Image to be automatically cropped.
:return: Cropped image
"""
# threshold image by Otsu number
bin_img = image <= self.otsu
# convert to binary as none ofthe function need the image ina non-binary format.
bin_img = bin_img.astype(bool)
# Remove holes in the leaf, but increases the size of rouge dots that are not the leaf.
bin_img = binary_closing(binary_dilation(bin_img))
bin_img = binary_dilation(binary_closing(bin_img))
# remove "islands" in the picture. Time consuming but effective in removing any erraneous dots.
bin_img = remove_small_objects(bin_img, min_size=100, connectivity=2)
# variables to store the number of pixels to cut the leaf by
h_crop = [None, None]
v_crop = [None, None]
# get the number of pixels to remove off of each side of the image.
# done by getting a sum of a row or column and testing if the row or column has nothing in
# each if-statement iterates through the image, however each if-statement transforms the image to get the
# desired crop margin
# it (sum of 0)
# From the top of the image
for count, row in enumerate(bin_img):
if np.any(row):
v_crop[1] = count - CROP_MARGIN
# From the bottom of the image (top flipped h)
for count, row in enumerate(bin_img[::-1, :]):
if np.any(row):
v_crop[0] = image.shape[0] - count + CROP_MARGIN
# From the left of the image (top 90 degrees clockwise)
for count, row in enumerate(bin_img.T):
if np.any(row):
h_crop[1] = count - CROP_MARGIN
# From the right of the image (top 90 degrees clockwise flipped h)
for count, row in enumerate(bin_img.T[::-1, :]):
if np.any(row):
h_crop[0] = image.shape[1] - count + CROP_MARGIN
return image[v_crop[0]:v_crop[1], h_crop[0]:h_crop[1]].copy()
# step 1: obtain scale
def get_scale(self):
"""
Measures the scale of the leaf image by iterating through the array
until pixels of the scale are found. Once points are found, the dist_form
function from scale_reader.py returns the scale.
:return: v_cm and h_cm, the vertical and horizontal measures of a centimeter
"""
# initialize values (for corners of scale's square)
v_pos1 = None
v_pos2 = None
h_pos1 = None
h_pos2 = None
# vertical
#
# top
for r_num, row in enumerate(self.scale_bin[::, ::]):
for c_num, column in enumerate(row):
if column:
v_pos1 = (c_num, r_num)
break
# bottom
for r_num, row in enumerate(self.scale_bin[::-1, ::]):
for c_num, column in enumerate(row):
if column:
v_pos2 = (c_num, self.scale_bin.shape[1] - r_num)
break
# horizontal
#
# left
for c_num, column in enumerate(self.scale_bin.T[::, ::]):
for r_num, row in enumerate(column):
if row:
h_pos1 = (c_num, r_num)
break
# right
for c_num, column in enumerate(self.scale_bin.T[::-1, ::]):
for r_num, row in enumerate(column):
if row:
h_pos2 = (self.scale_bin.shape[0] - c_num, r_num)
break
# calculate distance between points
v_cm = dist_form(v_pos1, v_pos2) / 2.0
h_cm = dist_form(h_pos1, h_pos2) / 2.0
# return veritcal and horizontal scale
return v_cm, h_cm
# chapter 2: measuring the leaf
# step 2: find presence of veins to assist with step 3
def measure_veins(
self,
adapthist_clip_limit=ADAPTHIST_CLIP_LIMIT,
denoise_weight=DENOISE_WEIGHT,
canny_sigma=CANNY_SIGMA,
hough_params=HOUGH_PARAMS,
min_total_lines=MIN_TOTAL_LINES,
min_center_lines=MIN_OTHER_LINES,
flat_line_margin=FLAT_LINE_DEGREES,
midrib_margin_percent=MIDRIB_MARGIN_PERCENT,
min_lines_in_group=MIN_OTHER_LINES):
"""
Measures the veins in a leaf. Split into several methods.
The parameters for the method is disgusting, but it avoids using global variables.
:param adapthist_clip_limit:
:param denoise_weight:
:param canny_sigma:
:param hough_params:
:param min_total_lines:
:param min_center_lines:
:param flat_line_margin:
:param midrib_margin_percent:
:param min_lines_in_group:
:return: {
'canny edges': vein_edges,
'hough above': [line for line, slope, angle in above],
'hough below': [line for line, slope, angle in below],
'hough center': midrib_lines,
'midvein': np.average([[p0[1], p1[1]] for p0, p1 in midrib_lines]),
'center range': midrib_range,
'midrib lin approx': lin_approx,
}
"""
# get the lines from the leaf
lines, vein_edges = self.__measure_veins_get_lines__(
adapthist_clip_limit=adapthist_clip_limit,
denoise_weight=denoise_weight,
canny_sigma=canny_sigma,
hough_params=hough_params)
# get the lines of the midrib
midrib_lines, midrib_range = self.__measure_veins_get_middle__(
lines=lines,
min_total_lines=min_total_lines,
min_center_lines=min_center_lines,
flat_line_margin=flat_line_margin,
midrib_margin_percent=midrib_margin_percent)
# get lines above and below midrib
above, below = self.__measure_veins_get_above_and_below__(
lines=lines,
midrib_lines=midrib_lines,
min_lines_in_group=min_lines_in_group)
# get the angle of the largest group of veins for above and below
# the angle is put in context of the midrib's angle for continuity
above_lines = [g[0] for g in above]
below_lines = [g[0] for g in below]
above_grouped = self.__measure_veins_group_veins__(above_lines)
below_grouped = self.__measure_veins_group_veins__(below_lines)
above_group_sums = [np.sum(group) for group in above_grouped]
above_largest_group = above_grouped[above_group_sums.index(np.max(above_group_sums))]
below_group_sums = [np.sum(group) for group in below_grouped]
below_largest_group = below_grouped[below_group_sums.index(np.max(below_group_sums))]
# get the midrib angle
midrib_angle = 0.0
for line in midrib_lines:
midrib_angle += line_to_angle(line)
midrib_angle /= len(midrib_lines)
# get the angles of lines above and below midrib
above_angle = 0.0
for line in above_largest_group:
above_angle += line_to_angle(line)
above_angle /= len(above_largest_group)
below_angle = 0.0
for line in below_largest_group:
midrib_angle += line_to_angle(line)
midrib_angle /= len(below_largest_group)
# put angles in context of midrib (subtract midrib angle from angle)
above_angle -= midrib_angle
above_angle = normalize_angle(above_angle)
below_angle -= midrib_angle
below_angle = normalize_angle(below_angle)
# get the length of the veins by simply taking the sum of the canny of the edges
vein_length = np.sum(vein_edges) / ((self.horizontal_cm_scale + self.vertical_cm_scale) / 2)
# get the linear approximation
lin_approx = self.__measure_veins_create_midrib_lin_approx__()
# finally return all of the data gained from measuring the veins
return {
'canny edges': vein_edges,
'hough above': [line for line, slope, angle in above],
'hough below': [line for line, slope, angle in below],
'hough center': midrib_lines,
'midvein': np.average([[p0[1], p1[1]] for p0, p1 in midrib_lines]),
'center range': midrib_range,
'midrib lin approx': lin_approx,
'angle above': above_angle,
'angle below': below_angle,
'vein length': vein_length
}
def __get_hough_params(self):
"""
1/16/17
This makes the hough params in relation to the array's size, since oyu cannot reference an obejct's self when
defining parameters.
This is to hopefully help the hough method pick up lines in the less detailed (smaller) images of the year 1
collection.
:return: tuple of (threshold, line_length, and line_gap)
"""
threshold = 10 # not entirely sure what this does, but it is assumed to not be as dependant on image sizes
length = self.leaf.shape[0]/32 # to get lines of length 50 in the new leaves on about 14 or so in the old
line_gap = 7 # to meet in the middle between 10 and 5
return (threshold, length, line_gap)
def __measure_veins_get_lines__(self,
adapthist_clip_limit=0.03,
denoise_weight=0.2,
canny_sigma=2.5,
hough_params=(10, 30, 10)):
"""
The first of the methods to measure the veins of a leaf. This gets the lines contained
in the veins by first curating the leaf (with an adaptive histogram transformation and
a denoising). Then, after the edges of the leaf are removed, the lines are found with
the Hough probalistic line transformation.
Mostly directly copy and pasted from old measure_veins method.
:rtype: list
:param adapthist_clip_limit:
:param denoise_weight:
:param canny_sigma:
:param hough_params: [threshold, line_length, and line_gap]
:return: lines
"""
# set the hough params to be local to the size of the leaf
hough_params = self.__get_hough_params()
# equalize the leaf to help the vein detection
try:
equalized = equalize_adapthist(self.leaf, clip_limit=adapthist_clip_limit)
except ValueError as e:
tb = sys.exc_info()[2]
traceback.print_tb(tb)
print(e)
raise MeasurementError(['Error at equalize_adapthist'])
except ZeroDivisionError as e:
tb = sys.exc_info()[2]
traceback.print_tb(tb)
print(e)
raise MeasurementError(['Error at equalize_adapthist'])
# try to remove any noise that could mess with the Hough function
denoised = denoise_tv_chambolle(equalized, weight=denoise_weight, multichannel=True)
# # make a copy of the array and set it as the bitmap
# leaf_bitmap = denoised.copy()
#
# # # only show leaf that is not background with threshold
# for row_count, row in enumerate(binary_dilation(self.leaf_bin.astype(bool))):
# for column_count, pixel in enumerate(row):
# if not pixel:
# leaf_bitmap[row_count, column_count] = 1
# use a numpy masked array to get only the grayscale leaf and no background
leaf_bitmap = np.ma.masked_array(denoised.copy(), mask=np.bool_(self.leaf_bin))
# find the edges with the Canny method
# sigma is set arbitrarily
edges = canny(leaf_bitmap, sigma=canny_sigma)
# try to make Canny's result more complete by closing gaps in the edges
edges = binary_closing(edges)
# remove the perimeter of the leaf, only leaving the veins inside
vein_edges = edges - np.logical_and(edges, -binary_erosion(self.leaf_bin))
# try and find all possible lines within the leaf
# threshold, line_length, and line_gap all set through trial and error
threshold, line_length, line_gap = hough_params
lines = probabilistic_hough_line(vein_edges,
threshold=threshold,
line_length=line_length,
line_gap=line_gap)
# return the lines found
return lines, vein_edges
def __measure_veins_get_middle__(self,
lines,
min_total_lines=MIN_TOTAL_LINES,
min_center_lines=MIN_OTHER_LINES,
flat_line_margin=FLAT_LINE_DEGREES,
midrib_margin_percent=0.05):
"""
Gets the line segments of the midrib
:param lines: all lines of the hough method
:param min_total_lines: fail leaf if not enough total lines
:param min_center_lines: fail leaf if not enough center lines
:param flat_line_margin: degrees of which a line is considered flat
:param midrib_margin_percent: percent of the leaf's width that is considered midrib
:return: midrib lines
"""
# fail the leaf if there are less than the minimum total number of lines
if len(lines) < min_total_lines:
# raise a error
raise MeasurementError(["Too few level lines in leaf. Only {0} when {1} needed.".format(
len(lines), min_total_lines)])
# filter lines that are within the margin of degrees determined by +- FLAT_LINE_DEGREES
level_lines = []
# find level lines, which have a good chance of representing the midrib
for l in lines:
# append leaf if within margin
if -flat_line_margin < line_to_angle(l) < flat_line_margin:
level_lines.append(l)
# get the median level line that will hopefully be the midrib.
approx_midrib_y = np.median([np.average([p0[1], p1[1]]) for p0, p1 in level_lines])
# calculate the range in which a line will be considered part of the midvein
midrib_range = (
approx_midrib_y - self.leaf_bin.shape[1] * midrib_margin_percent, # upper bound
approx_midrib_y + self.leaf_bin.shape[1] * midrib_margin_percent # lower bound
)
# collect all lines completely within the range
midrib_lines = []
# for all lines that are level
for p0, p1 in level_lines: # this could be changed to all lines to see what would happen
# if two points of line are both within the range, save it
if midrib_range[0] < p0[1] < midrib_range[1] and midrib_range[0] < p1[1] < midrib_range[1]:
midrib_lines.append((p0, p1))
# get the approximate center of the leaf based on the lines in the midrib
# prone to shifting where there are more level lines, not the actual midrib
center_y = np.average([[p0[1], p1[1]] for p0, p1 in midrib_lines])
center_x = np.average([[p0[0], p1[0]] for p0, p1 in midrib_lines])
# center_slope = np.average([get_slope(p0, p1) for p0, p1 in center_lines])
# fail the leaf if there are less than 8 level lines
if len(midrib_lines) < min_center_lines:
raise MeasurementError(
["Too few CENTER lines in leaf. Only {0} when {1} needed.".format(
len(midrib_lines), min_center_lines)])
# calculate average slope by going to degrees, averaging, then converting back to slope
degrees_measures = []
for p0, p1 in midrib_lines:
# convert line to degrees and append to list
degrees_measures.append(line_to_angle((p0, p1)))
# average the list
avg_degrees = np.average(degrees_measures)
# calculate the slope based on the degrees (more reliable than directly averaging slopes)
avg_slope = tan(radians(avg_degrees))
# parameters for the linear approximation if the midrib (for point slope form)
# y - center_y = avg_slope * (x - center_x)
self.midrib_func = [avg_slope, center_x, center_y]
return midrib_lines, midrib_range
def __measure_veins_get_above_and_below__(self,
lines,
midrib_lines,
min_lines_in_group=8):
"""
A method to get all lines above and below the midrib in two groups
:param lines: list of all lines
:param midrib_lines: list of lines in the midrib
:param min_lines_in_group: minimum number of lines allowed in a group (above or below)
:return: above, below (list of lines)
"""
# Since the midrib as well as the center line has been found, the leaf can now be separated
# based on the line above and below the midrib's center y value. This will deal with non-flat
# lines that are not in the midrib.
# get the approximate center of the leaf based on the lines in the midrib
# prone to shifting where there are more level lines, not the actual midrib
center_y = np.average([[p0[1], p1[1]] for p0, p1 in midrib_lines])
# separate lines based on above and below center line
above = []
below = []
# iterate through all lines
for l in lines:
# separate into points
p0, p1 = l
# check if the line is not a midrib line
if l not in midrib_lines:
# if both points' y values are below center y
if center_y <= p0[1] and center_y <= p1[1]:
below.append([l, line_to_slope(p0, p1), line_to_angle(l)])
# else if above center
elif center_y >= p0[1] and center_y >= p1[1]:
above.append([l, line_to_slope(p0, p1), line_to_angle(l)])
#########################################
# ABOVE #
#########################################
# # step 1:
# # filter the lines that are above the center and have a negative slope (pointing towards the midrib)
# # this assumes that all veins in the leaf point outwards (rather than in, towards the midrib)
# for l, slope, angle in above:
# # remove lines with a negative slope
# if not slope > 0.:
# above.remove([l, slope, angle])
#
# # step 2:
# # now filter based on lines that are close to the median line's degrees measure
# # calculate median degrees measure
# median_degrees = np.median([[angle] for l, slope, angle in above])
#
# # calculate margin
# margin = [median_degrees - degrees_margin,
# median_degrees + degrees_margin]
#
# # filter by margin
# for l, slope, angle in above:
# # remove if the line doesn't satisfy
# if not margin[0] < angle < margin[1]:
# above.remove([l, slope, angle])
# fail the leaf if there are less than 8 level lines
if len(above) < min_lines_in_group:
raise MeasurementError(
["Too few ABOVE lines in leaf. Only {0} when {1} needed.".format(
len(above), min_lines_in_group)])
#########################################
# BELOW #
#########################################
# # step 1:
# # filter the lines that are below the center and have a postive slope (pointing towards the midrib)
# # this assumes that all veins in the leaf point outwards (rather than in, towards the midrib)
# for l, slope, angle in below:
# # remove lines with a positive slope
# if not slope < 0.:
# below.remove([l, slope, angle])
#
# # step 2:
# # now filter based on lines that are close to the median line's degrees measure
# # calculate median degrees measure
# median_degrees = np.median([[angle] for l, slope, angle in below])
#
# # calculate margin
# margin = [median_degrees - degrees_margin,