megadetector 5.0.20__py3-none-any.whl → 5.0.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of megadetector might be problematic. Click here for more details.

Files changed (41) hide show
  1. megadetector/data_management/cct_json_utils.py +143 -7
  2. megadetector/data_management/cct_to_md.py +12 -5
  3. megadetector/data_management/databases/integrity_check_json_db.py +83 -77
  4. megadetector/data_management/importers/osu-small-animals-to-json.py +4 -4
  5. megadetector/data_management/importers/raic_csv_to_md_results.py +416 -0
  6. megadetector/data_management/importers/zamba_results_to_md_results.py +1 -2
  7. megadetector/data_management/lila/create_lila_test_set.py +25 -11
  8. megadetector/data_management/lila/download_lila_subset.py +9 -2
  9. megadetector/data_management/lila/generate_lila_per_image_labels.py +3 -2
  10. megadetector/data_management/lila/test_lila_metadata_urls.py +5 -1
  11. megadetector/data_management/read_exif.py +10 -14
  12. megadetector/data_management/rename_images.py +1 -1
  13. megadetector/data_management/yolo_output_to_md_output.py +18 -5
  14. megadetector/detection/process_video.py +14 -3
  15. megadetector/detection/pytorch_detector.py +15 -3
  16. megadetector/detection/run_detector.py +4 -3
  17. megadetector/detection/run_inference_with_yolov5_val.py +121 -13
  18. megadetector/detection/video_utils.py +40 -17
  19. megadetector/postprocessing/classification_postprocessing.py +1 -1
  20. megadetector/postprocessing/combine_api_outputs.py +1 -1
  21. megadetector/postprocessing/compare_batch_results.py +931 -142
  22. megadetector/postprocessing/detector_calibration.py +565 -0
  23. megadetector/postprocessing/md_to_coco.py +85 -19
  24. megadetector/postprocessing/postprocess_batch_results.py +32 -21
  25. megadetector/postprocessing/validate_batch_results.py +174 -64
  26. megadetector/taxonomy_mapping/map_new_lila_datasets.py +15 -12
  27. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +1 -1
  28. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +3 -1
  29. megadetector/utils/ct_utils.py +64 -2
  30. megadetector/utils/md_tests.py +15 -13
  31. megadetector/utils/path_utils.py +153 -37
  32. megadetector/utils/process_utils.py +9 -3
  33. megadetector/utils/write_html_image_list.py +21 -6
  34. megadetector/visualization/visualization_utils.py +329 -102
  35. megadetector/visualization/visualize_db.py +104 -63
  36. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/LICENSE +0 -0
  37. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/METADATA +143 -142
  38. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/RECORD +40 -39
  39. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/WHEEL +1 -1
  40. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/top_level.txt +0 -0
  41. megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +0 -359
@@ -40,6 +40,10 @@ EXIF_IMAGE_ROTATIONS = {
40
40
 
41
41
  TEXTALIGN_LEFT = 0
42
42
  TEXTALIGN_RIGHT = 1
43
+ TEXTALIGN_CENTER = 2
44
+
45
+ VTEXTALIGN_TOP = 0
46
+ VTEXTALIGN_BOTTOM = 1
43
47
 
44
48
  # Convert category ID from int to str
45
49
  DEFAULT_DETECTOR_LABEL_MAP = {
@@ -413,6 +417,7 @@ def render_detection_bounding_boxes(detections,
413
417
  max_classifications=3,
414
418
  colormap=None,
415
419
  textalign=TEXTALIGN_LEFT,
420
+ vtextalign=VTEXTALIGN_TOP,
416
421
  label_font_size=DEFAULT_LABEL_FONT_SIZE,
417
422
  custom_strings=None):
418
423
  """
@@ -469,37 +474,27 @@ def render_detection_bounding_boxes(detections,
469
474
  ]
470
475
 
471
476
  image (PIL.Image.Image): image on which we should render detections
472
-
473
477
  label_map (dict, optional): optional, mapping the numeric label to a string name. The type of the
474
478
  numeric label (typically strings) needs to be consistent with the keys in label_map; no casting is
475
479
  carried out. If [label_map] is None, no labels are shown (not even numbers and confidence values).
476
480
  If you want category numbers and confidence values without class labels, use the default value,
477
481
  the string 'show_categories'.
478
-
479
482
  classification_label_map (dict, optional): optional, mapping of the string class labels to the actual
480
483
  class names. The type of the numeric label (typically strings) needs to be consistent with the keys
481
484
  in label_map; no casting is carried out. If [label_map] is None, no labels are shown (not even numbers
482
485
  and confidence values).
483
-
484
486
  confidence_threshold (float or dict, optional), threshold above which boxes are rendered. Can also be a
485
- dictionary mapping category IDs to thresholds.
486
-
487
- thickness (int, optional): line thickness in pixels
488
-
489
- expansion (int, optional): number of pixels to expand bounding boxes on each side
490
-
487
+ dictionary mapping category IDs to thresholds.
488
+ thickness (int, optional): line thickness in pixels
489
+ expansion (int, optional): number of pixels to expand bounding boxes on each side
491
490
  classification_confidence_threshold (float, optional): confidence above which classification results
492
- are displayed
493
-
494
- max_classifications (int, optional): maximum number of classification results rendered for one image
495
-
491
+ are displayed
492
+ max_classifications (int, optional): maximum number of classification results rendered for one image
496
493
  colormap (list, optional): list of color names, used to choose colors for categories by
497
- indexing with the values in [classes]; defaults to a reasonable set of colors
498
-
499
- textalign (int, optional): TEXTALIGN_LEFT or TEXTALIGN_RIGHT
500
-
501
- label_font_size (float, optional): font size for labels
502
-
494
+ indexing with the values in [classes]; defaults to a reasonable set of colors
495
+ textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
496
+ vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
497
+ label_font_size (float, optional): font size for labels
503
498
  custom_strings: optional set of strings to append to detection labels, should have the
504
499
  same length as [detections]. Appended before any classification labels.
505
500
  """
@@ -613,7 +608,8 @@ def render_detection_bounding_boxes(detections,
613
608
 
614
609
  draw_bounding_boxes_on_image(image, display_boxes, classes,
615
610
  display_strs=display_strs, thickness=thickness,
616
- expansion=expansion, colormap=colormap, textalign=textalign,
611
+ expansion=expansion, colormap=colormap,
612
+ textalign=textalign, vtextalign=vtextalign,
617
613
  label_font_size=label_font_size)
618
614
 
619
615
  # ...render_detection_bounding_boxes(...)
@@ -627,6 +623,8 @@ def draw_bounding_boxes_on_image(image,
627
623
  display_strs=None,
628
624
  colormap=None,
629
625
  textalign=TEXTALIGN_LEFT,
626
+ vtextalign=VTEXTALIGN_TOP,
627
+ text_rotation=None,
630
628
  label_font_size=DEFAULT_LABEL_FONT_SIZE):
631
629
  """
632
630
  Draws bounding boxes on an image. Modifies the image in place.
@@ -647,7 +645,9 @@ def draw_bounding_boxes_on_image(image,
647
645
  or classification categories and/or confidence values.
648
646
  colormap (list, optional): list of color names, used to choose colors for categories by
649
647
  indexing with the values in [classes]; defaults to a reasonable set of colors
650
- textalign (int, optional): TEXTALIGN_LEFT or TEXTALIGN_RIGHT
648
+ textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
649
+ vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
650
+ text_rotation (float, optional): rotation to apply to text
651
651
  label_font_size (float, optional): font size for labels
652
652
  """
653
653
 
@@ -655,23 +655,55 @@ def draw_bounding_boxes_on_image(image,
655
655
  if not boxes_shape:
656
656
  return
657
657
  if len(boxes_shape) != 2 or boxes_shape[1] != 4:
658
- # print('Input must be of size [N, 4], but is ' + str(boxes_shape))
659
- return # no object detection on this image, return
658
+ return
660
659
  for i in range(boxes_shape[0]):
660
+ display_str_list = None
661
661
  if display_strs:
662
662
  display_str_list = display_strs[i]
663
- draw_bounding_box_on_image(image,
664
- boxes[i, 0], boxes[i, 1], boxes[i, 2], boxes[i, 3],
665
- classes[i],
666
- thickness=thickness, expansion=expansion,
667
- display_str_list=display_str_list,
668
- colormap=colormap,
669
- textalign=textalign,
670
- label_font_size=label_font_size)
663
+ draw_bounding_box_on_image(image,
664
+ boxes[i, 0], boxes[i, 1], boxes[i, 2], boxes[i, 3],
665
+ classes[i],
666
+ thickness=thickness, expansion=expansion,
667
+ display_str_list=display_str_list,
668
+ colormap=colormap,
669
+ textalign=textalign,
670
+ vtextalign=vtextalign,
671
+ text_rotation=text_rotation,
672
+ label_font_size=label_font_size)
671
673
 
672
674
  # ...draw_bounding_boxes_on_image(...)
673
675
 
674
676
 
677
+ def get_text_size(font,s):
678
+ """
679
+ Get the expected width and height when rendering the string [s] in the font
680
+ [font].
681
+
682
+ Args:
683
+ font (PIL.ImageFont): the font whose size we should query
684
+ s (str): the string whose size we should query
685
+
686
+ Returns:
687
+ tuple: (w,h), both floats in pixel coordinates
688
+ """
689
+
690
+ # This is what we did w/Pillow 9
691
+ # w,h = font.getsize(s)
692
+
693
+ # I would *think* this would be the equivalent for Pillow 10
694
+ # l,t,r,b = font.getbbox(s); w = r-l; h=b-t
695
+
696
+ # ...but this actually produces the most similar results to Pillow 9
697
+ # l,t,r,b = font.getbbox(s); w = r; h=b
698
+
699
+ try:
700
+ l,t,r,b = font.getbbox(s); w = r; h=b
701
+ except Exception:
702
+ w,h = font.getsize(s)
703
+
704
+ return w,h
705
+
706
+
675
707
  def draw_bounding_box_on_image(image,
676
708
  ymin,
677
709
  xmin,
@@ -684,7 +716,9 @@ def draw_bounding_box_on_image(image,
684
716
  use_normalized_coordinates=True,
685
717
  label_font_size=DEFAULT_LABEL_FONT_SIZE,
686
718
  colormap=None,
687
- textalign=TEXTALIGN_LEFT):
719
+ textalign=TEXTALIGN_LEFT,
720
+ vtextalign=VTEXTALIGN_TOP,
721
+ text_rotation=None):
688
722
  """
689
723
  Adds a bounding box to an image. Modifies the image in place.
690
724
 
@@ -717,7 +751,9 @@ def draw_bounding_box_on_image(image,
717
751
  label_font_size (float, optional): font size
718
752
  colormap (list, optional): list of color names, used to choose colors for categories by
719
753
  indexing with the values in [classes]; defaults to a reasonable set of colors
720
- textalign (int, optional): TEXTALIGN_LEFT or TEXTALIGN_RIGHT
754
+ textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
755
+ vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
756
+ text_rotation (float, optional): rotation to apply to text
721
757
  """
722
758
 
723
759
  if colormap is None:
@@ -768,71 +804,98 @@ def draw_bounding_box_on_image(image,
768
804
  draw.line([(left, top), (left, bottom), (right, bottom),
769
805
  (right, top), (left, top)], width=thickness, fill=color)
770
806
 
771
- try:
772
- font = ImageFont.truetype('arial.ttf', label_font_size)
773
- except IOError:
774
- font = ImageFont.load_default()
775
-
776
- def get_text_size(font,s):
807
+ if display_str_list is not None:
777
808
 
778
- # This is what we did w/Pillow 9
779
- # w,h = font.getsize(s)
780
-
781
- # I would *think* this would be the equivalent for Pillow 10
782
- # l,t,r,b = font.getbbox(s); w = r-l; h=b-t
783
-
784
- # ...but this actually produces the most similar results to Pillow 9
785
- # l,t,r,b = font.getbbox(s); w = r; h=b
786
-
787
809
  try:
788
- l,t,r,b = font.getbbox(s); w = r; h=b
789
- except Exception:
790
- w,h = font.getsize(s)
791
-
792
- return w,h
810
+ font = ImageFont.truetype('arial.ttf', label_font_size)
811
+ except IOError:
812
+ font = ImageFont.load_default()
793
813
 
794
- # If the total height of the display strings added to the top of the bounding
795
- # box exceeds the top of the image, stack the strings below the bounding box
796
- # instead of above.
797
- display_str_heights = [get_text_size(font,ds)[1] for ds in display_str_list]
798
-
799
- # Each display_str has a top and bottom margin of 0.05x.
800
- total_display_str_height = (1 + 2 * 0.05) * sum(display_str_heights)
801
-
802
- if top > total_display_str_height:
803
- text_bottom = top
804
- else:
805
- text_bottom = bottom + total_display_str_height
806
-
807
- # Reverse list and print from bottom to top.
808
- for display_str in display_str_list[::-1]:
809
-
810
- # Skip empty strings
811
- if len(display_str) == 0:
812
- continue
813
-
814
- text_width, text_height = get_text_size(font,display_str)
815
-
816
- text_left = left
817
-
818
- if textalign == TEXTALIGN_RIGHT:
819
- text_left = right - text_width
814
+ display_str_heights = [get_text_size(font,ds)[1] for ds in display_str_list]
815
+
816
+ # Each display_str has a top and bottom margin of 0.05x.
817
+ total_display_str_height = (1 + 2 * 0.05) * sum(display_str_heights)
818
+
819
+ # Reverse list and print from bottom to top
820
+ for i_str,display_str in enumerate(display_str_list[::-1]):
821
+
822
+ # Skip empty strings
823
+ if len(display_str) == 0:
824
+ continue
820
825
 
821
- margin = np.ceil(0.05 * text_height)
822
-
823
- draw.rectangle(
824
- [(text_left, text_bottom - text_height - 2 * margin), (text_left + text_width,
825
- text_bottom)],
826
- fill=color)
827
-
828
- draw.text(
829
- (text_left + margin, text_bottom - text_height - margin),
830
- display_str,
831
- fill='black',
832
- font=font)
833
-
834
- text_bottom -= (text_height + 2 * margin)
826
+ text_width, text_height = get_text_size(font,display_str)
827
+ margin = int(np.ceil(0.05 * text_height))
828
+
829
+ if text_rotation is not None and text_rotation != 0:
830
+
831
+ assert text_rotation == -90, \
832
+ 'Only -90-degree text rotation is supported'
833
+
834
+ image_tmp = Image.new('RGB',(text_width+2*margin,text_height+2*margin))
835
+ image_tmp_draw = ImageDraw.Draw(image_tmp)
836
+ image_tmp_draw.rectangle([0,0,text_width+2*margin,text_height+2*margin],fill=color)
837
+ image_tmp_draw.text( (margin,margin), display_str, font=font, fill='black')
838
+ rotated_text = image_tmp.rotate(text_rotation,expand=1)
839
+
840
+ if textalign == TEXTALIGN_RIGHT:
841
+ text_left = right
842
+ else:
843
+ text_left = left
844
+ text_left = int(text_left + (text_height) * i_str)
845
+
846
+ if vtextalign == VTEXTALIGN_BOTTOM:
847
+ text_top = bottom - text_width
848
+ else:
849
+ text_top = top
850
+ text_left = int(text_left)
851
+ text_top = int(text_top)
852
+
853
+ image.paste(rotated_text,[text_left,text_top])
854
+
855
+ else:
856
+
857
+ # If the total height of the display strings added to the top of the bounding
858
+ # box exceeds the top of the image, stack the strings below the bounding box
859
+ # instead of above, and vice-versa if we're bottom-aligning.
860
+ #
861
+ # If the text just doesn't fit outside the box, we don't try anything fancy,
862
+ # it will just appear outside the image.
863
+ if vtextalign == VTEXTALIGN_TOP:
864
+ text_bottom = top
865
+ if (text_bottom - total_display_str_height) < 0:
866
+ text_bottom = bottom + total_display_str_height
867
+ else:
868
+ assert vtextalign == VTEXTALIGN_BOTTOM, \
869
+ 'Unrecognized vertical text alignment {}'.format(vtextalign)
870
+ text_bottom = bottom + total_display_str_height
871
+ if (text_bottom + total_display_str_height) > im_height:
872
+ text_bottom = top
873
+
874
+ text_bottom = int(text_bottom) - i_str * (int(text_height + (2 * margin)))
875
+
876
+ text_left = left
877
+
878
+ if textalign == TEXTALIGN_RIGHT:
879
+ text_left = right - text_width
880
+ elif textalign == TEXTALIGN_CENTER:
881
+ text_left = ((right + left) / 2.0) - (text_width / 2.0)
882
+ text_left = int(text_left)
883
+
884
+ draw.rectangle(
885
+ [(text_left, (text_bottom - text_height) - (2 * margin)),
886
+ (text_left + text_width, text_bottom)],
887
+ fill=color)
888
+
889
+ draw.text(
890
+ (text_left + margin, text_bottom - text_height - margin),
891
+ display_str,
892
+ fill='black',
893
+ font=font)
894
+
895
+ # ...if we're rotating text
835
896
 
897
+ # ...if we're rendering text
898
+
836
899
  # ...def draw_bounding_box_on_image(...)
837
900
 
838
901
 
@@ -885,7 +948,12 @@ def render_db_bounding_boxes(boxes,
885
948
  original_size=None,
886
949
  label_map=None,
887
950
  thickness=DEFAULT_BOX_THICKNESS,
888
- expansion=0):
951
+ expansion=0,
952
+ colormap=None,
953
+ textalign=TEXTALIGN_LEFT,
954
+ vtextalign=VTEXTALIGN_TOP,
955
+ text_rotation=None,
956
+ label_font_size=DEFAULT_LABEL_FONT_SIZE):
889
957
  """
890
958
  Render bounding boxes (with class labels) on an image. This is a wrapper for
891
959
  draw_bounding_boxes_on_image, allowing the caller to operate on a resized image
@@ -907,6 +975,12 @@ def render_db_bounding_boxes(boxes,
907
975
  thickness (int, optional): line width
908
976
  expansion (int, optional): a number of pixels to include on each side of a cropped
909
977
  detection
978
+ colormap (list, optional): list of color names, used to choose colors for categories by
979
+ indexing with the values in [classes]; defaults to a reasonable set of colors
980
+ textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
981
+ vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
982
+ text_rotation (float, optional): rotation to apply to text
983
+ label_font_size (float, optional): font size for labels
910
984
  """
911
985
 
912
986
  display_boxes = []
@@ -944,7 +1018,12 @@ def render_db_bounding_boxes(boxes,
944
1018
  classes,
945
1019
  display_strs=display_strs,
946
1020
  thickness=thickness,
947
- expansion=expansion)
1021
+ expansion=expansion,
1022
+ colormap=colormap,
1023
+ textalign=textalign,
1024
+ vtextalign=vtextalign,
1025
+ text_rotation=text_rotation,
1026
+ label_font_size=label_font_size)
948
1027
 
949
1028
  # ...def render_db_bounding_boxes(...)
950
1029
 
@@ -968,11 +1047,11 @@ def draw_bounding_boxes_on_file(input_file,
968
1047
  Args:
969
1048
  input_file (str): filename or URL to load
970
1049
  output_file (str, optional): filename to which we should write the rendered image
971
- detections (list): a list of dictionaries with keys 'conf' and 'bbox';
1050
+ detections (list): a list of dictionaries with keys 'conf', 'bbox', and 'category';
972
1051
  boxes are length-four arrays formatted as [x,y,w,h], normalized,
973
- upper-left origin (this is the standard MD detection format)
1052
+ upper-left origin (this is the standard MD detection format). 'category' is a string-int.
974
1053
  detector_label_map (dict, optional): a dict mapping category IDs to strings. If this
975
- is None, no confidence values or identifiers are shown If this is {}, just category
1054
+ is None, no confidence values or identifiers are shown. If this is {}, just category
976
1055
  indices and confidence values are shown.
977
1056
  thickness (int, optional): line width in pixels for box rendering
978
1057
  expansion (int, optional): box expansion in pixels
@@ -1043,7 +1122,7 @@ def draw_db_boxes_on_file(input_file,
1043
1122
  classes = [0] * len(boxes)
1044
1123
 
1045
1124
  render_db_bounding_boxes(boxes, classes, image, original_size=None,
1046
- label_map=label_map, thickness=thickness, expansion=expansion)
1125
+ label_map=label_map, thickness=thickness, expansion=expansion)
1047
1126
 
1048
1127
  image.save(output_file)
1049
1128
 
@@ -1125,7 +1204,6 @@ def gray_scale_fraction(image,crop_size=(0.1,0.1)):
1125
1204
  if r == g and r == b and g == b:
1126
1205
  n_gray_pixels += 1
1127
1206
 
1128
-
1129
1207
  # ...def gray_scale_fraction(...)
1130
1208
 
1131
1209
 
@@ -1376,6 +1454,98 @@ def resize_image_folder(input_folder,
1376
1454
  # ...def resize_image_folder(...)
1377
1455
 
1378
1456
 
1457
+ def get_image_size(im,verbose=False):
1458
+ """
1459
+ Retrieve the size of an image. Returns None if the image fails to load.
1460
+
1461
+ Args:
1462
+ im (str or PIL.Image): filename or PIL image
1463
+
1464
+ Returns:
1465
+ tuple (w,h), or None if the image fails to load.
1466
+ """
1467
+
1468
+ image_name = '[in memory]'
1469
+
1470
+ try:
1471
+ if isinstance(im,str):
1472
+ image_name = im
1473
+ im = load_image(im)
1474
+ w = im.width
1475
+ h = im.height
1476
+ if w <= 0 or h <= 0:
1477
+ if verbose:
1478
+ print('Error reading width from image {}: {},{}'.format(
1479
+ image_name,w,h))
1480
+ return None
1481
+ return (w,h)
1482
+ except Exception as e:
1483
+ if verbose:
1484
+ print('Error reading width from image {}: {}'.format(
1485
+ image_name,str(e)))
1486
+ return None
1487
+
1488
+ # ...def get_image_size(...)
1489
+
1490
+
1491
+ def parallel_get_image_sizes(filenames,
1492
+ max_workers=16,
1493
+ use_threads=True,
1494
+ recursive=True,
1495
+ verbose=False):
1496
+ """
1497
+ Retrieve image sizes for a list or folder of images
1498
+
1499
+ Args:
1500
+ filenames (list or str): a list of image filenames or a folder
1501
+ max_workers (int, optional): the number of parallel workers to use; set to <=1 to disable
1502
+ parallelization
1503
+ use_threads (bool, optional): whether to use threads (True) or processes (False) for
1504
+ parallelization
1505
+ recursive (bool, optional): if [filenames] is a folder, whether to search recursively for images.
1506
+ Ignored if [filenames] is a list.
1507
+ verbose (bool, optional): enable additional debug output
1508
+
1509
+ Returns:
1510
+ dict: a dict mapping filenames to (w,h) tuples; the value will be None for images that fail
1511
+ to load.
1512
+ """
1513
+
1514
+ if isinstance(filenames,str) and os.path.isdir(filenames):
1515
+ if verbose:
1516
+ print('Enumerating images in {}'.format(filenames))
1517
+ filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
1518
+
1519
+ n_workers = min(max_workers,len(filenames))
1520
+
1521
+ if verbose:
1522
+ print('Getting image sizes for {} images'.format(len(filenames)))
1523
+
1524
+ if n_workers <= 1:
1525
+
1526
+ results = []
1527
+ for filename in filenames:
1528
+ results.append(get_image_size(filename,verbose=verbose))
1529
+
1530
+ else:
1531
+
1532
+ if use_threads:
1533
+ pool = ThreadPool(n_workers)
1534
+ else:
1535
+ pool = Pool(n_workers)
1536
+
1537
+ results = list(tqdm(pool.imap(
1538
+ partial(get_image_size,verbose=verbose),filenames), total=len(filenames)))
1539
+
1540
+ assert len(filenames) == len(results), 'Internal error in parallel_get_image_sizes'
1541
+
1542
+ to_return = {}
1543
+ for i_file,filename in enumerate(filenames):
1544
+ to_return[filename] = results[i_file]
1545
+
1546
+ return to_return
1547
+
1548
+
1379
1549
  #%% Image integrity checking functions
1380
1550
 
1381
1551
  def check_image_integrity(filename,modes=None):
@@ -1494,13 +1664,13 @@ def parallel_check_image_integrity(filenames,
1494
1664
  with either 'success' or 'error').
1495
1665
  """
1496
1666
 
1497
- n_workers = min(max_workers,len(filenames))
1498
-
1499
1667
  if isinstance(filenames,str) and os.path.isdir(filenames):
1500
1668
  if verbose:
1501
1669
  print('Enumerating images in {}'.format(filenames))
1502
1670
  filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
1503
1671
 
1672
+ n_workers = min(max_workers,len(filenames))
1673
+
1504
1674
  if verbose:
1505
1675
  print('Checking image integrity for {} filenames'.format(len(filenames)))
1506
1676
 
@@ -1527,6 +1697,63 @@ def parallel_check_image_integrity(filenames,
1527
1697
 
1528
1698
  if False:
1529
1699
 
1700
+ #%% Text rendering tests
1701
+
1702
+ import os # noqa
1703
+ import numpy as np # noqa
1704
+ from megadetector.visualization.visualization_utils import \
1705
+ draw_bounding_boxes_on_image, exif_preserving_save, load_image, \
1706
+ TEXTALIGN_LEFT,TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM,VTEXTALIGN_TOP, \
1707
+ DEFAULT_LABEL_FONT_SIZE
1708
+
1709
+ fn = os.path.expanduser('~\AppData\Local\Temp\md-tests\md-test-images\ena24_7904.jpg')
1710
+ output_fn = r'g:\temp\test.jpg'
1711
+
1712
+ image = load_image(fn)
1713
+
1714
+ w = 0.2; h = 0.2
1715
+ all_boxes = [[0.05, 0.05, 0.25, 0.25],
1716
+ [0.05, 0.35, 0.25, 0.6],
1717
+ [0.35, 0.05, 0.6, 0.25],
1718
+ [0.35, 0.35, 0.6, 0.6]]
1719
+
1720
+ alignments = [
1721
+ [TEXTALIGN_LEFT,VTEXTALIGN_TOP],
1722
+ [TEXTALIGN_LEFT,VTEXTALIGN_BOTTOM],
1723
+ [TEXTALIGN_RIGHT,VTEXTALIGN_TOP],
1724
+ [TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM]
1725
+ ]
1726
+
1727
+ labels = ['left_top','left_bottom','right_top','right_bottom']
1728
+
1729
+ text_rotation = -90
1730
+ n_label_copies = 2
1731
+
1732
+ for i_box,box in enumerate(all_boxes):
1733
+
1734
+ boxes = [box]
1735
+ boxes = np.array(boxes)
1736
+ classes = [i_box]
1737
+ display_strs = [[labels[i_box]]*n_label_copies]
1738
+ textalign = alignments[i_box][0]
1739
+ vtextalign = alignments[i_box][1]
1740
+ draw_bounding_boxes_on_image(image,
1741
+ boxes,
1742
+ classes,
1743
+ thickness=2,
1744
+ expansion=0,
1745
+ display_strs=display_strs,
1746
+ colormap=None,
1747
+ textalign=textalign,
1748
+ vtextalign=vtextalign,
1749
+ label_font_size=DEFAULT_LABEL_FONT_SIZE,
1750
+ text_rotation=text_rotation)
1751
+
1752
+ exif_preserving_save(image,output_fn)
1753
+ from megadetector.utils.path_utils import open_file
1754
+ open_file(output_fn)
1755
+
1756
+
1530
1757
  #%% Recursive resize test
1531
1758
 
1532
1759
  from megadetector.visualization.visualization_utils import resize_image_folder # noqa