mapreader 1.3.5__tar.gz → 1.3.7__tar.gz
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.
- {mapreader-1.3.5 → mapreader-1.3.7}/PKG-INFO +2 -1
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/__init__.py +25 -10
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/_version.py +3 -3
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/annotate/annotator.py +62 -18
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/annotate/utils.py +4 -1
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/classify/classifier.py +33 -33
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/classify/load_annotations.py +36 -6
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/download/data_structures.py +14 -6
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/download/downloader.py +20 -6
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/download/downloader_utils.py +8 -2
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/download/sheet_downloader.py +13 -1
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/download/tile_merging.py +4 -3
- mapreader-1.3.5/mapreader/process/post_process.py → mapreader-1.3.7/mapreader/process/context_post_process.py +1 -1
- mapreader-1.3.7/mapreader/process/occlusion_analysis.py +307 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/spot_text/deepsolo_runner.py +32 -8
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/spot_text/dptext_detr_runner.py +25 -5
- mapreader-1.3.7/mapreader/spot_text/maptext_runner.py +511 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/spot_text/runner_base.py +63 -1
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader.egg-info/PKG-INFO +2 -1
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader.egg-info/SOURCES.txt +4 -5
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader.egg-info/requires.txt +1 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/setup.py +1 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/tests/test_geo_pipeline.py +1 -1
- mapreader-1.3.5/tests/test_annotator.py +0 -276
- mapreader-1.3.5/tests/test_post_processing.py +0 -171
- mapreader-1.3.5/tests/test_sheet_downloader.py +0 -767
- {mapreader-1.3.5 → mapreader-1.3.7}/LICENSE +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/README.md +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/__main__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/annotate/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/classify/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/classify/custom_models.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/classify/datasets.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/download/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/download/tile_loading.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/load/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/load/geo_utils.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/load/images.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/load/loader.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/process/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/process/process.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/spot_text/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/utils/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/utils/compute_and_save_stats.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader/utils/slice_parallel.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader.egg-info/dependency_links.txt +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader.egg-info/entry_points.txt +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader.egg-info/not-zip-safe +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/mapreader.egg-info/top_level.txt +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/setup.cfg +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/tests/test_import.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.7}/versioneer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mapreader
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.7
|
|
4
4
|
Summary: A computer vision pipeline for the semantic exploration of maps/images at scale
|
|
5
5
|
Home-page: https://github.com/Living-with-machines/MapReader
|
|
6
6
|
Download-URL: https://github.com/Living-with-machines/MapReader/archive/refs/heads/main.zip
|
|
@@ -58,6 +58,7 @@ Requires-Dist: geopandas<1.0.0
|
|
|
58
58
|
Requires-Dist: pyogrio>=0.7.2
|
|
59
59
|
Requires-Dist: cartopy>=0.22.0
|
|
60
60
|
Requires-Dist: joblib>=1.4.0
|
|
61
|
+
Requires-Dist: opencv-python<5.0.0.0
|
|
61
62
|
Provides-Extra: dev
|
|
62
63
|
Requires-Dist: pytest<9.0.0; extra == "dev"
|
|
63
64
|
Requires-Dist: pytest-cov<6.0.0,>=4.1.0; extra == "dev"
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
+
# download
|
|
2
|
+
from mapreader.download.sheet_downloader import SheetDownloader
|
|
3
|
+
from mapreader.download.downloader import Downloader
|
|
4
|
+
from mapreader.download.downloader_utils import create_polygon_from_latlons, create_line_from_latlons
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# load
|
|
1
8
|
from mapreader.load.images import MapImages
|
|
2
9
|
from mapreader.load.loader import loader
|
|
3
10
|
from mapreader.load.loader import load_patches
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
from mapreader.
|
|
7
|
-
from mapreader.download.downloader_utils import create_polygon_from_latlons, create_line_from_latlons
|
|
12
|
+
# annotate
|
|
13
|
+
from mapreader.annotate.annotator import Annotator
|
|
8
14
|
|
|
15
|
+
# classify
|
|
9
16
|
from mapreader.classify.load_annotations import AnnotationsLoader
|
|
10
17
|
from mapreader.classify.datasets import PatchDataset
|
|
11
18
|
from mapreader.classify.datasets import PatchContextDataset
|
|
12
19
|
from mapreader.classify.classifier import ClassifierContainer
|
|
13
20
|
from mapreader.classify import custom_models
|
|
14
21
|
|
|
22
|
+
# spot_text
|
|
15
23
|
try:
|
|
16
24
|
from mapreader.spot_text.deepsolo_runner import DeepSoloRunner
|
|
17
25
|
except ImportError:
|
|
@@ -22,19 +30,26 @@ try:
|
|
|
22
30
|
except ImportError:
|
|
23
31
|
pass
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
try:
|
|
34
|
+
from mapreader.spot_text.maptext_runner import MapTextRunner
|
|
35
|
+
except ImportError:
|
|
36
|
+
pass
|
|
28
37
|
|
|
29
|
-
from . import _version
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
# post process
|
|
40
|
+
from mapreader.process.context_post_process import ContextPostProcessor
|
|
41
|
+
from mapreader.process.occlusion_analysis import OcclusionAnalyzer
|
|
32
42
|
|
|
43
|
+
# utils
|
|
33
44
|
from mapreader.load import geo_utils
|
|
34
45
|
|
|
35
|
-
import mapreader
|
|
36
46
|
|
|
47
|
+
# version
|
|
48
|
+
from . import _version
|
|
49
|
+
__version__ = _version.get_versions()["version"]
|
|
50
|
+
|
|
51
|
+
import mapreader
|
|
37
52
|
|
|
38
53
|
def print_version():
|
|
39
54
|
"""Print the current version of mapreader."""
|
|
40
|
-
print(mapreader.__version__)
|
|
55
|
+
print(mapreader.__version__)
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2024-08-
|
|
11
|
+
"date": "2024-08-12T15:00:36+0100",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "1.3.
|
|
14
|
+
"full-revisionid": "6eec8310fe00efb31c03d0f4618dd7dd8a8ebd46",
|
|
15
|
+
"version": "1.3.7"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -55,6 +55,8 @@ class Annotator:
|
|
|
55
55
|
Name of the column in which labels are stored in patch DataFrame, by default "label"
|
|
56
56
|
show_context : bool, optional
|
|
57
57
|
Whether to show context when loading patches, by default False
|
|
58
|
+
border : bool, optional
|
|
59
|
+
Whether to add a border around the central patch when showing context, by default False
|
|
58
60
|
auto_save : bool, optional
|
|
59
61
|
Whether to automatically save annotations, by default True
|
|
60
62
|
delimiter : str, optional
|
|
@@ -108,6 +110,7 @@ class Annotator:
|
|
|
108
110
|
patch_paths_col: str = "image_path",
|
|
109
111
|
label_col: str = "label",
|
|
110
112
|
show_context: bool = False,
|
|
113
|
+
border: bool = False,
|
|
111
114
|
auto_save: bool = True,
|
|
112
115
|
delimiter: str = ",",
|
|
113
116
|
sortby: str | None = None,
|
|
@@ -255,6 +258,7 @@ class Annotator:
|
|
|
255
258
|
self.patch_paths_col = patch_paths_col
|
|
256
259
|
self.annotations_file = annotations_file
|
|
257
260
|
self.show_context = show_context
|
|
261
|
+
self.border = border
|
|
258
262
|
self.auto_save = auto_save
|
|
259
263
|
self.username = username
|
|
260
264
|
self.task_name = task_name
|
|
@@ -452,7 +456,7 @@ class Annotator:
|
|
|
452
456
|
|
|
453
457
|
self.navbox = widgets.VBox([widgets.HBox([prev_btn, next_btn])])
|
|
454
458
|
|
|
455
|
-
def
|
|
459
|
+
def _get_queue(
|
|
456
460
|
self, as_type: str | None = "list"
|
|
457
461
|
) -> list[int] | (pd.Index | pd.Series):
|
|
458
462
|
"""
|
|
@@ -520,7 +524,7 @@ class Annotator:
|
|
|
520
524
|
return indices
|
|
521
525
|
return queue_df
|
|
522
526
|
|
|
523
|
-
def
|
|
527
|
+
def _get_context(self):
|
|
524
528
|
"""
|
|
525
529
|
Provides the surrounding context for the patch to be annotated.
|
|
526
530
|
|
|
@@ -531,15 +535,21 @@ class Annotator:
|
|
|
531
535
|
context.
|
|
532
536
|
"""
|
|
533
537
|
|
|
534
|
-
def
|
|
538
|
+
def get_square(image_path, dim=True, border=False):
|
|
535
539
|
# Resize the image
|
|
536
540
|
im = Image.open(image_path)
|
|
537
541
|
|
|
538
542
|
# Dim the image
|
|
539
|
-
if dim in [True,
|
|
543
|
+
if dim in ["True", True]:
|
|
540
544
|
im_array = np.array(im)
|
|
541
545
|
im_array = 256 - (256 - im_array) * 0.4 # lighten image
|
|
542
546
|
im = Image.fromarray(im_array.astype(np.uint8))
|
|
547
|
+
|
|
548
|
+
if border in ["True", True] and self.border:
|
|
549
|
+
w, h = im.size
|
|
550
|
+
im = ImageOps.expand(im, border=2, fill="red")
|
|
551
|
+
im = im.resize((w, h))
|
|
552
|
+
|
|
543
553
|
return im
|
|
544
554
|
|
|
545
555
|
def get_empty_square(patch_size: tuple[int, int]):
|
|
@@ -593,7 +603,10 @@ class Annotator:
|
|
|
593
603
|
|
|
594
604
|
# derive ids from items
|
|
595
605
|
ids = [x.index[0] if len(x.index) == 1 else None for x in items]
|
|
596
|
-
|
|
606
|
+
# list of booleans, True if not the current patch, False if the current patch
|
|
607
|
+
# used for dimming the surrounding patches and adding a border to the current patch
|
|
608
|
+
dim_bools = [x != ix for x in ids]
|
|
609
|
+
border_bools = [x == ix for x in ids]
|
|
597
610
|
|
|
598
611
|
# derive images from items
|
|
599
612
|
image_paths = [
|
|
@@ -601,14 +614,16 @@ class Annotator:
|
|
|
601
614
|
]
|
|
602
615
|
|
|
603
616
|
# zip them
|
|
604
|
-
image_list = list(zip(image_paths,
|
|
617
|
+
image_list = list(zip(image_paths, dim_bools, border_bools))
|
|
605
618
|
|
|
606
619
|
# split them into rows
|
|
607
620
|
per_row = len(deltas)
|
|
608
621
|
images = [
|
|
609
622
|
[
|
|
610
|
-
|
|
611
|
-
|
|
623
|
+
get_square(image_path, dim=dim, border=border)
|
|
624
|
+
if image_path
|
|
625
|
+
else get_empty_square((width, height))
|
|
626
|
+
for image_path, dim, border in lst
|
|
612
627
|
]
|
|
613
628
|
for lst in array_split(image_list, per_row)
|
|
614
629
|
]
|
|
@@ -641,6 +656,7 @@ class Annotator:
|
|
|
641
656
|
def annotate(
|
|
642
657
|
self,
|
|
643
658
|
show_context: bool | None = None,
|
|
659
|
+
border: bool | None = None,
|
|
644
660
|
sortby: str | None = None,
|
|
645
661
|
ascending: bool | None = None,
|
|
646
662
|
min_values: dict | None = None,
|
|
@@ -648,6 +664,7 @@ class Annotator:
|
|
|
648
664
|
surrounding: int | None = None,
|
|
649
665
|
resize_to: int | None = None,
|
|
650
666
|
max_size: int | None = None,
|
|
667
|
+
show_vals: list[str] | None = None,
|
|
651
668
|
) -> None:
|
|
652
669
|
"""Annotate at the patch-level of the current patch.
|
|
653
670
|
Renders the annotation interface for the first image.
|
|
@@ -657,6 +674,9 @@ class Annotator:
|
|
|
657
674
|
show_context : bool or None, optional
|
|
658
675
|
Whether or not to display the surrounding context for each image.
|
|
659
676
|
Default is None.
|
|
677
|
+
border : bool or None, optional
|
|
678
|
+
Whether or not to display a border around the image (when using `show_context`).
|
|
679
|
+
Default is None.
|
|
660
680
|
sortby : str or None, optional
|
|
661
681
|
Name of the column to use to sort the patch DataFrame, by default None.
|
|
662
682
|
Default sort order is ``ascending=True``. Pass ``ascending=False`` keyword argument to sort in descending order.
|
|
@@ -677,12 +697,18 @@ class Annotator:
|
|
|
677
697
|
max_size : int or None, optional
|
|
678
698
|
The size in pixels for the longest side to which constrain each
|
|
679
699
|
patch image. Default: 100.
|
|
700
|
+
resize_to : int or None, optional
|
|
701
|
+
The size in pixels for the longest side to which resize each patch image. Default: None.
|
|
702
|
+
show_vals : list[str] or None, optional
|
|
703
|
+
List of column names to show in the display. By default, None.
|
|
680
704
|
|
|
681
705
|
Notes
|
|
682
706
|
-----
|
|
683
707
|
This method is a wrapper for the
|
|
684
708
|
:meth:`~.annotate.annotator.Annotate._annotate` method.
|
|
685
709
|
"""
|
|
710
|
+
if border is not None:
|
|
711
|
+
self.border = border
|
|
686
712
|
if sortby is not None:
|
|
687
713
|
self._sortby = sortby
|
|
688
714
|
if ascending is not None:
|
|
@@ -693,8 +719,10 @@ class Annotator:
|
|
|
693
719
|
if max_values is not None:
|
|
694
720
|
self._max_values = max_values
|
|
695
721
|
|
|
722
|
+
self.show_vals = show_vals
|
|
723
|
+
|
|
696
724
|
# re-set up queue using new min/max values
|
|
697
|
-
self._queue = self.
|
|
725
|
+
self._queue = self._get_queue()
|
|
698
726
|
|
|
699
727
|
self._annotate(
|
|
700
728
|
show_context=show_context,
|
|
@@ -743,7 +771,7 @@ class Annotator:
|
|
|
743
771
|
self.max_size = max_size
|
|
744
772
|
|
|
745
773
|
# re-set up queue
|
|
746
|
-
self._queue = self.
|
|
774
|
+
self._queue = self._get_queue()
|
|
747
775
|
|
|
748
776
|
if self._filter_for is not None:
|
|
749
777
|
print(f"[INFO] Filtering for: {self._filter_for}")
|
|
@@ -767,7 +795,7 @@ class Annotator:
|
|
|
767
795
|
Previous index, current index, and path of the current image.
|
|
768
796
|
"""
|
|
769
797
|
if self.current_index == len(self._queue):
|
|
770
|
-
self.
|
|
798
|
+
self._render_complete()
|
|
771
799
|
return
|
|
772
800
|
|
|
773
801
|
self.previous_index = self.current_index
|
|
@@ -777,7 +805,7 @@ class Annotator:
|
|
|
777
805
|
|
|
778
806
|
img_path = self.patch_df.at[ix, self.patch_paths_col]
|
|
779
807
|
|
|
780
|
-
self.
|
|
808
|
+
self._render()
|
|
781
809
|
return self.previous_index, self.current_index, img_path
|
|
782
810
|
|
|
783
811
|
def _prev_example(self, *_) -> tuple[int, int, str]:
|
|
@@ -790,7 +818,7 @@ class Annotator:
|
|
|
790
818
|
Previous index, current index, and path of the current image.
|
|
791
819
|
"""
|
|
792
820
|
if self.current_index == len(self._queue):
|
|
793
|
-
self.
|
|
821
|
+
self._render_complete()
|
|
794
822
|
return
|
|
795
823
|
|
|
796
824
|
if self.current_index > 0:
|
|
@@ -801,10 +829,10 @@ class Annotator:
|
|
|
801
829
|
|
|
802
830
|
img_path = self.patch_df.at[ix, self.patch_paths_col]
|
|
803
831
|
|
|
804
|
-
self.
|
|
832
|
+
self._render()
|
|
805
833
|
return self.previous_index, self.current_index, img_path
|
|
806
834
|
|
|
807
|
-
def
|
|
835
|
+
def _render(self) -> None:
|
|
808
836
|
"""
|
|
809
837
|
Displays the image at the current index in the annotation interface.
|
|
810
838
|
|
|
@@ -817,7 +845,7 @@ class Annotator:
|
|
|
817
845
|
"""
|
|
818
846
|
# Check whether we have reached the end
|
|
819
847
|
if self.current_index >= len(self) - 1:
|
|
820
|
-
self.
|
|
848
|
+
self._render_complete()
|
|
821
849
|
return
|
|
822
850
|
|
|
823
851
|
ix = self._queue[self.current_index]
|
|
@@ -841,7 +869,7 @@ class Annotator:
|
|
|
841
869
|
clear_output(wait=True)
|
|
842
870
|
image = self.get_patch_image(ix)
|
|
843
871
|
if self.show_context:
|
|
844
|
-
context = self.
|
|
872
|
+
context = self._get_context()
|
|
845
873
|
self._context_image = context
|
|
846
874
|
display(context.convert("RGB"))
|
|
847
875
|
else:
|
|
@@ -852,6 +880,22 @@ class Annotator:
|
|
|
852
880
|
text = f'<p><a href="{url}" target="_blank">Click to see entire map.</a></p>'
|
|
853
881
|
add_ins += [widgets.HTML(text)]
|
|
854
882
|
|
|
883
|
+
if self.show_vals:
|
|
884
|
+
patch_info = []
|
|
885
|
+
for col in self.show_vals:
|
|
886
|
+
if col in self.patch_df.columns:
|
|
887
|
+
val = self.patch_df.at[ix, col]
|
|
888
|
+
if isinstance(val, float):
|
|
889
|
+
val = f"{val:.4g}"
|
|
890
|
+
patch_info.append(f"<b>{col}</b>: {val}")
|
|
891
|
+
add_ins += [
|
|
892
|
+
widgets.HTML(
|
|
893
|
+
'<p style="text-align: center">'
|
|
894
|
+
+ "<br>".join(patch_info)
|
|
895
|
+
+ "</p>"
|
|
896
|
+
)
|
|
897
|
+
]
|
|
898
|
+
|
|
855
899
|
value = self.current_index + 1 if self.current_index else 1
|
|
856
900
|
description = f"{value} / {len(self._queue)}"
|
|
857
901
|
add_ins += [
|
|
@@ -983,7 +1027,7 @@ class Annotator:
|
|
|
983
1027
|
_filter = ~self.patch_df[self.label_col].isna()
|
|
984
1028
|
return self.patch_df[_filter]
|
|
985
1029
|
|
|
986
|
-
def
|
|
1030
|
+
def _render_complete(self):
|
|
987
1031
|
"""
|
|
988
1032
|
Renders the completion message once all images have been annotated.
|
|
989
1033
|
|
|
@@ -310,7 +310,10 @@ def annotation_interface(
|
|
|
310
310
|
# stream=True so we don't download the whole page, only check if
|
|
311
311
|
# the page exists
|
|
312
312
|
response = requests.get(url, stream=True)
|
|
313
|
-
|
|
313
|
+
if not response.status_code < 400:
|
|
314
|
+
raise RuntimeError(
|
|
315
|
+
f"URL could not get a response: {response.status_code}"
|
|
316
|
+
)
|
|
314
317
|
print()
|
|
315
318
|
print(f"URL: {url}")
|
|
316
319
|
except:
|
|
@@ -86,8 +86,8 @@ class ClassifierContainer:
|
|
|
86
86
|
The optimizer being used for training the model.
|
|
87
87
|
scheduler : None or torch.optim.lr_scheduler._LRScheduler
|
|
88
88
|
The learning rate scheduler being used for training the model.
|
|
89
|
-
|
|
90
|
-
The
|
|
89
|
+
loss_fn : None or nn.modules.loss._Loss
|
|
90
|
+
The loss function to use for training the model.
|
|
91
91
|
metrics : dict
|
|
92
92
|
A dictionary to store the metrics computed during training.
|
|
93
93
|
last_epoch : int
|
|
@@ -159,7 +159,7 @@ class ClassifierContainer:
|
|
|
159
159
|
|
|
160
160
|
self.optimizer = None
|
|
161
161
|
self.scheduler = None
|
|
162
|
-
self.
|
|
162
|
+
self.loss_fn = None
|
|
163
163
|
|
|
164
164
|
self.metrics = {}
|
|
165
165
|
self.last_epoch = 0
|
|
@@ -399,54 +399,54 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
|
|
|
399
399
|
|
|
400
400
|
self.scheduler = scheduler
|
|
401
401
|
|
|
402
|
-
def
|
|
403
|
-
self,
|
|
402
|
+
def add_loss_fn(
|
|
403
|
+
self, loss_fn: str | nn.modules.loss._Loss | None = "cross entropy"
|
|
404
404
|
) -> None:
|
|
405
405
|
"""
|
|
406
|
-
Add a loss
|
|
406
|
+
Add a loss function to the classifier object.
|
|
407
407
|
|
|
408
408
|
Parameters
|
|
409
409
|
----------
|
|
410
|
-
|
|
411
|
-
The loss
|
|
410
|
+
loss_fn : str or torch.nn.modules.loss._Loss
|
|
411
|
+
The loss function to add to the classifier object.
|
|
412
412
|
Accepted string values are "cross entropy" or "ce" (cross-entropy), "bce" (binary cross-entropy) and "mse" (mean squared error).
|
|
413
413
|
|
|
414
414
|
Returns
|
|
415
415
|
-------
|
|
416
416
|
None
|
|
417
|
-
The function only modifies the ``
|
|
417
|
+
The function only modifies the ``loss_fn`` attribute of the
|
|
418
418
|
classifier and does not return anything.
|
|
419
419
|
"""
|
|
420
|
-
if isinstance(
|
|
421
|
-
if
|
|
422
|
-
|
|
423
|
-
elif
|
|
420
|
+
if isinstance(loss_fn, str):
|
|
421
|
+
if loss_fn in ["cross entropy", "ce", "cross_entropy", "cross-entropy"]:
|
|
422
|
+
loss_fn = nn.CrossEntropyLoss()
|
|
423
|
+
elif loss_fn in [
|
|
424
424
|
"bce",
|
|
425
425
|
"binary_cross_entropy",
|
|
426
426
|
"binary cross entropy",
|
|
427
427
|
"binary cross-entropy",
|
|
428
428
|
]:
|
|
429
|
-
|
|
430
|
-
elif
|
|
429
|
+
loss_fn = nn.BCELoss()
|
|
430
|
+
elif loss_fn in [
|
|
431
431
|
"mse",
|
|
432
432
|
"mean_square_error",
|
|
433
433
|
"mean_squared_error",
|
|
434
434
|
"mean squared error",
|
|
435
435
|
]:
|
|
436
|
-
|
|
436
|
+
loss_fn = nn.MSELoss()
|
|
437
437
|
else:
|
|
438
438
|
raise NotImplementedError(
|
|
439
|
-
'[ERROR] At present, if passing ``
|
|
439
|
+
'[ERROR] At present, if passing ``loss_fn`` as a string, the loss function can only be "cross entropy" or "ce" (cross-entropy), "bce" (binary cross-entropy) or "mse" (mean squared error).'
|
|
440
440
|
)
|
|
441
441
|
|
|
442
|
-
print(f'[INFO] Using "{
|
|
442
|
+
print(f'[INFO] Using "{loss_fn}" as loss function.')
|
|
443
443
|
|
|
444
|
-
elif not isinstance(
|
|
444
|
+
elif not isinstance(loss_fn, nn.modules.loss._Loss):
|
|
445
445
|
raise ValueError(
|
|
446
|
-
'[ERROR] Please pass ``
|
|
446
|
+
'[ERROR] Please pass ``loss_fn`` as a string ("cross entropy", "bce" or "mse") or torch.nn loss function (see https://pytorch.org/docs/stable/nn.html).'
|
|
447
447
|
)
|
|
448
448
|
|
|
449
|
-
self.
|
|
449
|
+
self.loss_fn = loss_fn
|
|
450
450
|
|
|
451
451
|
def model_summary(
|
|
452
452
|
self,
|
|
@@ -643,7 +643,7 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
|
|
|
643
643
|
|
|
644
644
|
def train_component_summary(self) -> None:
|
|
645
645
|
"""
|
|
646
|
-
Print a summary of the optimizer,
|
|
646
|
+
Print a summary of the optimizer, loss function, and trainable model
|
|
647
647
|
components.
|
|
648
648
|
|
|
649
649
|
Returns:
|
|
@@ -655,8 +655,8 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
|
|
|
655
655
|
print("* Optimizer:")
|
|
656
656
|
print(str(self.optimizer))
|
|
657
657
|
print(divider)
|
|
658
|
-
print("*
|
|
659
|
-
print(str(self.
|
|
658
|
+
print("* Loss function:")
|
|
659
|
+
print(str(self.loss_fn))
|
|
660
660
|
print(divider)
|
|
661
661
|
print("* Model:")
|
|
662
662
|
self.model_summary(trainable_col=True)
|
|
@@ -783,9 +783,9 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
|
|
|
783
783
|
Raises
|
|
784
784
|
------
|
|
785
785
|
ValueError
|
|
786
|
-
If the
|
|
787
|
-
:meth:`~.classify.classifier.ClassifierContainer.
|
|
788
|
-
method to set the
|
|
786
|
+
If the loss function is not set. Use the
|
|
787
|
+
:meth:`~.classify.classifier.ClassifierContainer.add_loss_fn`
|
|
788
|
+
method to set the loss function.
|
|
789
789
|
|
|
790
790
|
If the optimizer is not set and the phase is "train". Use the
|
|
791
791
|
:meth:`~.classify.classifier.ClassifierContainer.initialize_optimizer`
|
|
@@ -894,10 +894,10 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to add one." # noqa
|
|
|
894
894
|
# summing the final output and the auxiliary
|
|
895
895
|
# output but in testing we only consider the
|
|
896
896
|
# final output.
|
|
897
|
-
if self.
|
|
897
|
+
if self.loss_fn is None:
|
|
898
898
|
raise ValueError(
|
|
899
|
-
"[ERROR]
|
|
900
|
-
Use ``
|
|
899
|
+
"[ERROR] Loss function is not yet defined.\n\n\
|
|
900
|
+
Use ``add_loss_fn`` to define one."
|
|
901
901
|
)
|
|
902
902
|
|
|
903
903
|
if self.is_inception and (
|
|
@@ -910,8 +910,8 @@ Use ``add_criterion`` to define one."
|
|
|
910
910
|
if not isinstance(aux_outputs, torch.Tensor):
|
|
911
911
|
aux_outputs = self._get_logits(aux_outputs)
|
|
912
912
|
|
|
913
|
-
loss1 = self.
|
|
914
|
-
loss2 = self.
|
|
913
|
+
loss1 = self.loss_fn(outputs, label_indices)
|
|
914
|
+
loss2 = self.loss_fn(aux_outputs, label_indices)
|
|
915
915
|
# https://discuss.pytorch.org/t/how-to-optimize-inception-model-with-auxiliary-classifiers/7958
|
|
916
916
|
loss = loss1 + 0.4 * loss2
|
|
917
917
|
|
|
@@ -921,7 +921,7 @@ Use ``add_criterion`` to define one."
|
|
|
921
921
|
if not isinstance(outputs, torch.Tensor):
|
|
922
922
|
outputs = self._get_logits(outputs)
|
|
923
923
|
|
|
924
|
-
loss = self.
|
|
924
|
+
loss = self.loss_fn(outputs, label_indices)
|
|
925
925
|
|
|
926
926
|
_, pred_label_indices = torch.max(outputs, dim=1)
|
|
927
927
|
|
|
@@ -25,6 +25,7 @@ class AnnotationsLoader:
|
|
|
25
25
|
|
|
26
26
|
def __init__(self):
|
|
27
27
|
self.annotations = pd.DataFrame()
|
|
28
|
+
self.labels_map = {}
|
|
28
29
|
self.reviewed = pd.DataFrame()
|
|
29
30
|
self.patch_paths_col = None
|
|
30
31
|
self.label_col = None
|
|
@@ -33,6 +34,7 @@ class AnnotationsLoader:
|
|
|
33
34
|
def load(
|
|
34
35
|
self,
|
|
35
36
|
annotations: str | pd.DataFrame,
|
|
37
|
+
labels_map: dict | None = None,
|
|
36
38
|
delimiter: str | None = ",",
|
|
37
39
|
images_dir: str | None = None,
|
|
38
40
|
remove_broken: bool | None = True,
|
|
@@ -52,6 +54,8 @@ class AnnotationsLoader:
|
|
|
52
54
|
annotations : Union[str, pd.DataFrame]
|
|
53
55
|
The annotations.
|
|
54
56
|
Can either be the path to a csv file or a pandas.DataFrame.
|
|
57
|
+
labels_map : Optional[dict], optional
|
|
58
|
+
A dictionary mapping labels to indices. If not provided, labels will be mapped to indices based on the order in which they appear in the annotations dataframe. By default None.
|
|
55
59
|
delimiter : Optional[str], optional
|
|
56
60
|
The delimiter to use when loading the csv file as a dataframe, by default ",".
|
|
57
61
|
images_dir : Optional[str], optional
|
|
@@ -132,8 +136,25 @@ class AnnotationsLoader:
|
|
|
132
136
|
|
|
133
137
|
self.unique_labels = self.annotations[self.label_col].unique().tolist()
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
# if labels_map is explicitly provided
|
|
140
|
+
if labels_map:
|
|
141
|
+
self.labels_map = dict(
|
|
142
|
+
sorted(labels_map.items())
|
|
143
|
+
) # sort labels_map by keys
|
|
144
|
+
if not set(self.unique_labels).issubset(set(labels_map.values())):
|
|
145
|
+
raise ValueError(
|
|
146
|
+
"[ERROR] There are label(s) in the annotations that are not in the labels map. Please check the labels_map."
|
|
147
|
+
)
|
|
148
|
+
# if inferring labels_map
|
|
149
|
+
else:
|
|
150
|
+
if append:
|
|
151
|
+
for label in self.unique_labels:
|
|
152
|
+
if label not in self.labels_map.values():
|
|
153
|
+
self.labels_map[len(self.labels_map)] = label
|
|
154
|
+
else:
|
|
155
|
+
# reset labels map
|
|
156
|
+
labels_map = {i: label for i, label in enumerate(self.unique_labels)}
|
|
157
|
+
self.labels_map = labels_map
|
|
137
158
|
|
|
138
159
|
self.annotations["label_index"] = self.annotations[self.label_col].apply(
|
|
139
160
|
self._get_label_index
|
|
@@ -451,10 +472,13 @@ Please check your image paths and update them if necessary.'
|
|
|
451
472
|
self.reviewed.loc[input_id, "label_index"] = self._get_label_index(
|
|
452
473
|
input_label
|
|
453
474
|
)
|
|
454
|
-
|
|
475
|
+
if not (
|
|
455
476
|
self.annotations[self.label_col].value_counts().tolist()
|
|
456
477
|
== self.annotations["label_index"].value_counts().tolist()
|
|
457
|
-
)
|
|
478
|
+
):
|
|
479
|
+
raise RuntimeError(
|
|
480
|
+
f"[ERROR] Label indices do not match label counts. Please check the label indices for label '{input_label}'."
|
|
481
|
+
)
|
|
458
482
|
print(
|
|
459
483
|
f'[INFO] Image {input_id} has been relabelled as "{input_label}"'
|
|
460
484
|
)
|
|
@@ -607,12 +631,18 @@ Please check your image paths and update them if necessary.'
|
|
|
607
631
|
test_size=float(relative_frac_test),
|
|
608
632
|
random_state=random_state,
|
|
609
633
|
)
|
|
610
|
-
|
|
634
|
+
if not len(self.annotations) == len(df_train) + len(df_val) + len(df_test):
|
|
635
|
+
raise ValueError(
|
|
636
|
+
"[ERROR] Number of annotations in the split dataframes does not match the number of annotations in the original dataframe."
|
|
637
|
+
)
|
|
611
638
|
|
|
612
639
|
else:
|
|
613
640
|
df_val = df_temp
|
|
614
641
|
df_test = None
|
|
615
|
-
|
|
642
|
+
if not len(self.annotations) == len(df_train) + len(df_val):
|
|
643
|
+
raise ValueError(
|
|
644
|
+
"[ERROR] Number of annotations in the split dataframes does not match the number of annotations in the original dataframe."
|
|
645
|
+
)
|
|
616
646
|
|
|
617
647
|
if context_datasets:
|
|
618
648
|
datasets = self.create_patch_context_datasets(
|
|
@@ -14,8 +14,11 @@ class Coordinate:
|
|
|
14
14
|
lon : float
|
|
15
15
|
longitude value (in range [-180°, 180°] )
|
|
16
16
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if not -90 <= lat <= 90:
|
|
18
|
+
raise ValueError("Latitude must be in range [-90, 90]")
|
|
19
|
+
if not -180 <= lon <= 180:
|
|
20
|
+
raise ValueError("Longitude must be in range [-180, 180]")
|
|
21
|
+
|
|
19
22
|
self.lat = lat
|
|
20
23
|
self.lon = lon
|
|
21
24
|
|
|
@@ -37,9 +40,12 @@ class GridIndex:
|
|
|
37
40
|
z : int
|
|
38
41
|
Zoom level
|
|
39
42
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
if not z >= 0:
|
|
44
|
+
raise ValueError("Zoom level must be greater than or equal to 0")
|
|
45
|
+
if not 0 <= x < 2**z:
|
|
46
|
+
raise ValueError(f"X value must be in range [0, {2**z}]")
|
|
47
|
+
if not 0 <= y < 2**z:
|
|
48
|
+
raise ValueError(f"Y value must be in range [0, {2**z}]")
|
|
43
49
|
self.x = x
|
|
44
50
|
self.y = y
|
|
45
51
|
self.z = z
|
|
@@ -61,7 +67,9 @@ class GridBoundingBox:
|
|
|
61
67
|
cell1 : GridIndex
|
|
62
68
|
cell2 : GridIndex
|
|
63
69
|
"""
|
|
64
|
-
|
|
70
|
+
if cell1.z != cell2.z:
|
|
71
|
+
raise NotImplementedError("Can't calculate a grid on different scales yet")
|
|
72
|
+
|
|
65
73
|
start_x = min(cell1.x, cell2.x)
|
|
66
74
|
end_x = max(cell1.x, cell2.x)
|
|
67
75
|
start_y = min(cell1.y, cell2.y)
|