mapreader 1.3.2__tar.gz → 1.3.4__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.
Files changed (51) hide show
  1. {mapreader-1.3.2 → mapreader-1.3.4}/PKG-INFO +1 -1
  2. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/_version.py +3 -3
  3. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/load/images.py +18 -8
  4. mapreader-1.3.4/mapreader/spot_text/deepsolo_runner.py +335 -0
  5. mapreader-1.3.4/mapreader/spot_text/dptext_detr_runner.py +153 -0
  6. mapreader-1.3.2/mapreader/spot_text/dptext_detr_runner.py → mapreader-1.3.4/mapreader/spot_text/runner_base.py +176 -150
  7. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/PKG-INFO +1 -1
  8. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/SOURCES.txt +1 -0
  9. mapreader-1.3.2/mapreader/spot_text/deepsolo_runner.py +0 -643
  10. {mapreader-1.3.2 → mapreader-1.3.4}/LICENSE +0 -0
  11. {mapreader-1.3.2 → mapreader-1.3.4}/README.md +0 -0
  12. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/__init__.py +0 -0
  13. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/__main__.py +0 -0
  14. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/annotate/__init__.py +0 -0
  15. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/annotate/annotator.py +0 -0
  16. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/annotate/utils.py +0 -0
  17. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/__init__.py +0 -0
  18. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/classifier.py +0 -0
  19. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/custom_models.py +0 -0
  20. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/datasets.py +0 -0
  21. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/classify/load_annotations.py +0 -0
  22. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/__init__.py +0 -0
  23. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/data_structures.py +0 -0
  24. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/downloader.py +0 -0
  25. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/downloader_utils.py +0 -0
  26. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/sheet_downloader.py +0 -0
  27. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/tile_loading.py +0 -0
  28. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/download/tile_merging.py +0 -0
  29. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/load/__init__.py +0 -0
  30. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/load/geo_utils.py +0 -0
  31. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/load/loader.py +0 -0
  32. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/process/__init__.py +0 -0
  33. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/process/post_process.py +0 -0
  34. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/process/process.py +0 -0
  35. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/spot_text/__init__.py +0 -0
  36. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/utils/__init__.py +0 -0
  37. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/utils/compute_and_save_stats.py +0 -0
  38. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader/utils/slice_parallel.py +0 -0
  39. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/dependency_links.txt +0 -0
  40. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/entry_points.txt +0 -0
  41. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/not-zip-safe +0 -0
  42. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/requires.txt +0 -0
  43. {mapreader-1.3.2 → mapreader-1.3.4}/mapreader.egg-info/top_level.txt +0 -0
  44. {mapreader-1.3.2 → mapreader-1.3.4}/setup.cfg +0 -0
  45. {mapreader-1.3.2 → mapreader-1.3.4}/setup.py +0 -0
  46. {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_annotator.py +0 -0
  47. {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_geo_pipeline.py +0 -0
  48. {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_import.py +0 -0
  49. {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_post_processing.py +0 -0
  50. {mapreader-1.3.2 → mapreader-1.3.4}/tests/test_sheet_downloader.py +0 -0
  51. {mapreader-1.3.2 → mapreader-1.3.4}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mapreader
3
- Version: 1.3.2
3
+ Version: 1.3.4
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
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-05-15T11:10:51+0100",
11
+ "date": "2024-07-05T11:21:59+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "282b8733c2c83412b979c2c234adda1432691862",
15
- "version": "1.3.2"
14
+ "full-revisionid": "add823a499b1d0763327f9d56962c2a6af0e2d42",
15
+ "version": "1.3.4"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -990,6 +990,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
990
990
  output_format: str | None = "png",
991
991
  rewrite: bool | None = False,
992
992
  verbose: bool | None = False,
993
+ overlap: int = 0,
993
994
  ) -> None:
994
995
  """
995
996
  Patchify all images in the specified ``tree_level`` and (if ``add_to_parents=True``) add the patches to the MapImages instance's ``images`` dictionary.
@@ -1024,6 +1025,8 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1024
1025
  verbose : bool, optional
1025
1026
  If True, progress updates will be printed throughout, by default
1026
1027
  ``False``.
1028
+ overlap : int, optional
1029
+ Fractional overlap between patches, by default ``0``.
1027
1030
 
1028
1031
  Returns
1029
1032
  -------
@@ -1088,6 +1091,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1088
1091
  output_format=output_format,
1089
1092
  rewrite=rewrite,
1090
1093
  verbose=verbose,
1094
+ overlap=overlap,
1091
1095
  )
1092
1096
 
1093
1097
  def _patchify_by_pixel(
@@ -1100,6 +1104,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1100
1104
  output_format: str | None = "png",
1101
1105
  rewrite: bool | None = False,
1102
1106
  verbose: bool | None = False,
1107
+ overlap: int | None = 0,
1103
1108
  ):
1104
1109
  """Patchify one image and (if ``add_to_parents=True``) add the patch to the MapImages instance's ``images`` dictionary.
1105
1110
 
@@ -1123,6 +1128,8 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1123
1128
  verbose : bool, optional
1124
1129
  If True, progress updates will be printed throughout, by default
1125
1130
  ``False``.
1131
+ overlap : int, optional
1132
+ Fractional overlap between patches, by default ``0``.
1126
1133
  """
1127
1134
  tree_level = self._get_tree_level(image_id)
1128
1135
 
@@ -1140,15 +1147,14 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1140
1147
 
1141
1148
  height, width = img.height, img.width
1142
1149
 
1143
- for x in range(0, width, patch_size):
1144
- for y in range(0, height, patch_size):
1150
+ x = 0
1151
+ while x < width:
1152
+ y = 0
1153
+ while y < height:
1145
1154
  max_x = min(x + patch_size, width)
1146
1155
  max_y = min(y + patch_size, height)
1147
1156
 
1148
- min_x = x
1149
- min_y = y
1150
-
1151
- patch_id = f"patch-{min_x}-{min_y}-{max_x}-{max_y}-#{image_id}#.{output_format}"
1157
+ patch_id = f"patch-{x}-{y}-{max_x}-{max_y}-#{image_id}#.{output_format}"
1152
1158
  patch_path = os.path.join(path_save, patch_id)
1153
1159
  patch_path = os.path.abspath(patch_path)
1154
1160
 
@@ -1158,7 +1164,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1158
1164
  )
1159
1165
 
1160
1166
  else:
1161
- patch = img.crop((min_x, min_y, max_x, max_y))
1167
+ patch = img.crop((x, y, max_x, max_y))
1162
1168
  if max_x == width:
1163
1169
  patch = ImageOps.pad(
1164
1170
  patch, (patch_size, patch.height), centering=(0, 0)
@@ -1181,11 +1187,15 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1181
1187
  image_path=patch_path,
1182
1188
  parent_path=parent_path,
1183
1189
  tree_level="patch",
1184
- pixel_bounds=(min_x, min_y, max_x, max_y),
1190
+ pixel_bounds=(x, y, max_x, max_y),
1185
1191
  )
1186
1192
  self._add_patch_coords_id(patch_id)
1187
1193
  self._add_patch_polygons_id(patch_id)
1188
1194
 
1195
+ overlap_pixels = int(patch_size * overlap)
1196
+ y = y + patch_size - overlap_pixels
1197
+ x = x + patch_size - overlap_pixels
1198
+
1189
1199
  def _patchify_by_pixel_square(
1190
1200
  self,
1191
1201
  image_id: str,
@@ -0,0 +1,335 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+ import pickle
5
+
6
+ try:
7
+ import adet
8
+ except ImportError:
9
+ raise ImportError(
10
+ "[ERROR] Please install DeepSolo from the following link: https://github.com/rwood-97/DeepSolo"
11
+ )
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+ from adet.config import get_cfg
16
+
17
+ try:
18
+ from detectron2.engine import DefaultPredictor
19
+ except ImportError:
20
+ raise ImportError("[ERROR] Please install Detectron2")
21
+
22
+ from shapely import LineString, MultiPolygon, Polygon
23
+
24
+ # first assert we are using the deep solo version of adet
25
+ if adet.__version__ != "0.2.0-deepsolo":
26
+ raise ImportError(
27
+ "[ERROR] Please install DeepSolo from the following link: https://github.com/rwood-97/DeepSolo"
28
+ )
29
+
30
+ from .runner_base import Runner
31
+
32
+
33
+ class DeepSoloRunner(Runner):
34
+ def __init__(
35
+ self,
36
+ patch_df: pd.DataFrame = None,
37
+ parent_df: pd.DataFrame = None,
38
+ cfg_file: str
39
+ | pathlib.Path = "./DeepSolo/configs/R_50/IC15/finetune_150k_tt_mlt_13_15_textocr.yaml",
40
+ weights_file: str
41
+ | pathlib.Path = "./ic15_res50_finetune_synth-tt-mlt-13-15-textocr.pth",
42
+ device: str = "cpu",
43
+ ) -> None:
44
+ # setup the dataframes
45
+ self.patch_df = patch_df
46
+ self.parent_df = parent_df
47
+
48
+ # set up predictions as dictionaries
49
+ self.patch_predictions = {}
50
+ self.parent_predictions = {}
51
+ self.geo_predictions = {}
52
+
53
+ # setup the config
54
+ cfg = get_cfg() # get a fresh new config
55
+ cfg.merge_from_file(cfg_file)
56
+ cfg.MODEL.WEIGHTS = weights_file
57
+ cfg.MODEL.DEVICE = device
58
+
59
+ self.voc_size = cfg.MODEL.TRANSFORMER.VOC_SIZE
60
+ self.use_customer_dictionary = cfg.MODEL.TRANSFORMER.CUSTOM_DICT
61
+ if self.voc_size == 96:
62
+ self.CTLABELS = [
63
+ " ",
64
+ "!",
65
+ '"',
66
+ "#",
67
+ "$",
68
+ "%",
69
+ "&",
70
+ "'",
71
+ "(",
72
+ ")",
73
+ "*",
74
+ "+",
75
+ ",",
76
+ "-",
77
+ ".",
78
+ "/",
79
+ "0",
80
+ "1",
81
+ "2",
82
+ "3",
83
+ "4",
84
+ "5",
85
+ "6",
86
+ "7",
87
+ "8",
88
+ "9",
89
+ ":",
90
+ ";",
91
+ "<",
92
+ "=",
93
+ ">",
94
+ "?",
95
+ "@",
96
+ "A",
97
+ "B",
98
+ "C",
99
+ "D",
100
+ "E",
101
+ "F",
102
+ "G",
103
+ "H",
104
+ "I",
105
+ "J",
106
+ "K",
107
+ "L",
108
+ "M",
109
+ "N",
110
+ "O",
111
+ "P",
112
+ "Q",
113
+ "R",
114
+ "S",
115
+ "T",
116
+ "U",
117
+ "V",
118
+ "W",
119
+ "X",
120
+ "Y",
121
+ "Z",
122
+ "[",
123
+ "\\",
124
+ "]",
125
+ "^",
126
+ "_",
127
+ "`",
128
+ "a",
129
+ "b",
130
+ "c",
131
+ "d",
132
+ "e",
133
+ "f",
134
+ "g",
135
+ "h",
136
+ "i",
137
+ "j",
138
+ "k",
139
+ "l",
140
+ "m",
141
+ "n",
142
+ "o",
143
+ "p",
144
+ "q",
145
+ "r",
146
+ "s",
147
+ "t",
148
+ "u",
149
+ "v",
150
+ "w",
151
+ "x",
152
+ "y",
153
+ "z",
154
+ "{",
155
+ "|",
156
+ "}",
157
+ "~",
158
+ ]
159
+ elif self.voc_size == 37:
160
+ self.CTLABELS = [
161
+ "a",
162
+ "b",
163
+ "c",
164
+ "d",
165
+ "e",
166
+ "f",
167
+ "g",
168
+ "h",
169
+ "i",
170
+ "j",
171
+ "k",
172
+ "l",
173
+ "m",
174
+ "n",
175
+ "o",
176
+ "p",
177
+ "q",
178
+ "r",
179
+ "s",
180
+ "t",
181
+ "u",
182
+ "v",
183
+ "w",
184
+ "x",
185
+ "y",
186
+ "z",
187
+ "0",
188
+ "1",
189
+ "2",
190
+ "3",
191
+ "4",
192
+ "5",
193
+ "6",
194
+ "7",
195
+ "8",
196
+ "9",
197
+ ]
198
+ else:
199
+ with open(self.use_customer_dictionary, "rb") as fp:
200
+ self.CTLABELS = pickle.load(fp)
201
+ # voc_size includes the unknown class, which is not in self.CTABLES
202
+ assert int(self.voc_size - 1) == len(
203
+ self.CTLABELS
204
+ ), f"voc_size is not matched dictionary size, got {int(self.voc_size - 1)} and {len(self.CTLABELS)}."
205
+
206
+ # setup the predictor
207
+ self.predictor = DefaultPredictor(cfg)
208
+
209
+ def get_patch_predictions(
210
+ self,
211
+ outputs: dict,
212
+ return_dataframe: bool = False,
213
+ min_ioa: float = 0.7,
214
+ ) -> dict | pd.DataFrame:
215
+ """Post process the model outputs to get patch predictions.
216
+
217
+ Parameters
218
+ ----------
219
+ outputs : dict
220
+ The outputs from the model.
221
+ return_dataframe : bool, optional
222
+ Whether to return the predictions as a pandas DataFrame, by default False
223
+ min_ioa : float, optional
224
+ The minimum intersection over area to consider two polygons the same, by default 0.7
225
+
226
+ Returns
227
+ -------
228
+ dict or pd.DataFrame
229
+ A dictionary containing the patch predictions or a DataFrame if `as_dataframe` is True.
230
+ """
231
+ # key for predictions
232
+ image_id = outputs["image_id"]
233
+ self.patch_predictions[image_id] = []
234
+
235
+ # get instances
236
+ instances = outputs["instances"].to("cpu")
237
+ ctrl_pnts = instances.ctrl_points.numpy()
238
+ scores = instances.scores.tolist()
239
+ recs = instances.recs
240
+ bd_pts = np.asarray(instances.bd)
241
+
242
+ self._post_process(image_id, ctrl_pnts, scores, recs, bd_pts)
243
+ self._deduplicate(image_id, min_ioa=min_ioa)
244
+
245
+ if return_dataframe:
246
+ return self._dict_to_dataframe(self.patch_predictions, geo=False)
247
+ return self.patch_predictions
248
+
249
+ def _process_ctrl_pnt(self, pnt):
250
+ points = pnt.reshape(-1, 2)
251
+ return points
252
+
253
+ def _ctc_decode_recognition(self, rec):
254
+ last_char = "###"
255
+ s = ""
256
+ for c in rec:
257
+ c = int(c)
258
+ if c < self.voc_size - 1:
259
+ if last_char != c:
260
+ if self.voc_size == 37 or self.voc_size == 96:
261
+ s += self.CTLABELS[c]
262
+ last_char = c
263
+ else:
264
+ s += str(chr(self.CTLABELS[c]))
265
+ last_char = c
266
+ else:
267
+ last_char = "###"
268
+ return s
269
+
270
+ def _post_process(self, image_id, ctrl_pnts, scores, recs, bd_pnts, alpha=0.4):
271
+ for ctrl_pnt, score, rec, bd in zip(ctrl_pnts, scores, recs, bd_pnts):
272
+ # draw polygons
273
+ if bd is not None:
274
+ bd = np.hsplit(bd, 2)
275
+ bd = np.vstack([bd[0], bd[1][::-1]])
276
+ polygon = Polygon(bd).buffer(0)
277
+
278
+ if isinstance(polygon, MultiPolygon):
279
+ polygon = polygon.convex_hull
280
+
281
+ # draw center lines
282
+ line = self._process_ctrl_pnt(ctrl_pnt)
283
+ line = LineString(line)
284
+
285
+ # draw text
286
+ text = self._ctc_decode_recognition(rec)
287
+ if self.voc_size == 37:
288
+ text = text.upper()
289
+ # text = "{:.2f}: {}".format(score, text)
290
+ text = f"{text}"
291
+ score = f"{score:.2f}"
292
+
293
+ self.patch_predictions[image_id].append([polygon, text, score])
294
+
295
+ @staticmethod
296
+ def _dict_to_dataframe(
297
+ preds: dict,
298
+ geo: bool = False,
299
+ parent: bool = False,
300
+ ) -> pd.DataFrame:
301
+ """Convert the predictions dictionary to a pandas DataFrame.
302
+
303
+ Parameters
304
+ ----------
305
+ preds : dict
306
+ A dictionary of predictions.
307
+ geo : bool, optional
308
+ Whether the dictionary is georeferenced coords (or pixel bounds), by default True
309
+ parent : bool, optional
310
+ Whether the dictionary is at parent level, by default False
311
+
312
+ Returns
313
+ -------
314
+ pd.DataFrame
315
+ A pandas DataFrame containing the predictions.
316
+ """
317
+ if geo:
318
+ columns = ["polygon", "crs", "text", "score"]
319
+ else:
320
+ columns = ["polygon", "text", "score"]
321
+
322
+ if parent:
323
+ columns.append("patch_id")
324
+
325
+ preds_df = pd.concat(
326
+ pd.DataFrame(
327
+ preds[k],
328
+ index=np.full(len(preds[k]), k),
329
+ columns=columns,
330
+ )
331
+ for k in preds.keys()
332
+ )
333
+ preds_df.index.name = "image_id"
334
+ preds_df.reset_index(inplace=True) # reset index to get image_id as a column
335
+ return preds_df
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+
5
+ try:
6
+ import adet
7
+ except ImportError:
8
+ raise ImportError(
9
+ "[ERROR] Please install DPText-DETR from the following link: https://github.com/rwood-97/DPText-DETR"
10
+ )
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+ from adet.config import get_cfg
15
+
16
+ try:
17
+ from detectron2.engine import DefaultPredictor
18
+ except ImportError:
19
+ raise ImportError("[ERROR] Please install Detectron2")
20
+
21
+ from shapely import MultiPolygon, Polygon
22
+
23
+ # first assert we are using the dptext detr version of adet
24
+ if adet.__version__ != "0.2.0-dptext-detr":
25
+ raise ImportError(
26
+ "[ERROR] Please install DPText-DETR from the following link: https://github.com/rwood-97/DPText-DETR"
27
+ )
28
+
29
+ from .runner_base import Runner
30
+
31
+
32
+ class DPTextDETRRunner(Runner):
33
+ def __init__(
34
+ self,
35
+ patch_df: pd.DataFrame = None,
36
+ parent_df: pd.DataFrame = None,
37
+ cfg_file: str
38
+ | pathlib.Path = "./DPText-DETR/configs/DPText_DETR/ArT/R_50_poly.yaml",
39
+ weights_file: str | pathlib.Path = "./art_final.pth",
40
+ device: str = "cpu",
41
+ ) -> None:
42
+ # setup the dataframes
43
+ self.patch_df = patch_df
44
+ self.parent_df = parent_df
45
+
46
+ # set up predictions as dictionaries
47
+ self.patch_predictions = {}
48
+ self.parent_predictions = {}
49
+ self.geo_predictions = {}
50
+
51
+ # setup the config
52
+ cfg = get_cfg() # get a fresh new config
53
+ cfg.merge_from_file(cfg_file)
54
+ cfg.MODEL.WEIGHTS = weights_file
55
+ cfg.MODEL.DEVICE = device
56
+
57
+ # setup the predictor
58
+ self.predictor = DefaultPredictor(cfg)
59
+
60
+ def get_patch_predictions(
61
+ self,
62
+ outputs: dict,
63
+ return_dataframe: bool = False,
64
+ min_ioa: float = 0.7,
65
+ ) -> dict | pd.DataFrame:
66
+ """Post process the model outputs to get patch predictions.
67
+
68
+ Parameters
69
+ ----------
70
+ outputs : dict
71
+ The outputs from the model.
72
+ return_dataframe : bool, optional
73
+ Whether to return the predictions as a pandas DataFrame, by default False
74
+ min_ioa : float, optional
75
+ The minimum intersection over area to consider two polygons the same, by default 0.7
76
+
77
+ Returns
78
+ -------
79
+ dict or pd.DataFrame
80
+ A dictionary containing the patch predictions or a DataFrame if `as_dataframe` is True.
81
+ """
82
+ # key for predictions
83
+ image_id = outputs["image_id"]
84
+ self.patch_predictions[image_id] = []
85
+
86
+ # get instances
87
+ instances = outputs["instances"].to("cpu")
88
+ scores = instances.scores.tolist()
89
+ pred_classes = instances.pred_classes.tolist()
90
+ bd_pts = np.asarray(instances.polygons)
91
+
92
+ self._post_process(image_id, scores, pred_classes, bd_pts)
93
+ self._deduplicate(image_id, min_ioa=min_ioa)
94
+
95
+ if return_dataframe:
96
+ return self._dict_to_dataframe(self.patch_predictions, geo=False)
97
+ return self.patch_predictions
98
+
99
+ def _post_process(self, image_id, scores, pred_classes, bd_pnts):
100
+ for score, _pred_class, bd in zip(scores, pred_classes, bd_pnts):
101
+ # draw polygons
102
+ if bd is not None:
103
+ bd = bd.reshape(-1, 2)
104
+ polygon = Polygon(bd).buffer(0)
105
+
106
+ if isinstance(polygon, MultiPolygon):
107
+ polygon = polygon.convex_hull
108
+
109
+ score = f"{score:.2f}"
110
+
111
+ self.patch_predictions[image_id].append([polygon, score])
112
+
113
+ @staticmethod
114
+ def _dict_to_dataframe(
115
+ preds: dict,
116
+ geo: bool = False,
117
+ parent: bool = False,
118
+ ) -> pd.DataFrame:
119
+ """Convert the predictions dictionary to a pandas DataFrame.
120
+
121
+ Parameters
122
+ ----------
123
+ preds : dict
124
+ A dictionary of predictions.
125
+ geo : bool, optional
126
+ Whether the dictionary is georeferenced coords (or pixel bounds), by default True
127
+ parent : bool, optional
128
+ Whether the dictionary is at the parent level, by default False
129
+
130
+ Returns
131
+ -------
132
+ pd.DataFrame
133
+ A pandas DataFrame containing the predictions.
134
+ """
135
+ if geo:
136
+ columns = ["polygon", "crs", "score"]
137
+ else:
138
+ columns = ["polygon", "score"]
139
+
140
+ if parent:
141
+ columns.append("patch_id")
142
+
143
+ preds_df = pd.concat(
144
+ pd.DataFrame(
145
+ preds[k],
146
+ index=np.full(len(preds[k]), k),
147
+ columns=columns,
148
+ )
149
+ for k in preds.keys()
150
+ )
151
+ preds_df.index.name = "image_id"
152
+ preds_df.reset_index(inplace=True)
153
+ return preds_df