geoai-py 0.2.2__py2.py3-none-any.whl → 0.3.0__py2.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.
geoai/extract.py CHANGED
@@ -14,6 +14,7 @@ import rasterio
14
14
  from rasterio.windows import Window
15
15
  from rasterio.features import shapes
16
16
  from huggingface_hub import hf_hub_download
17
+ from .preprocess import get_raster_stats
17
18
 
18
19
 
19
20
  class BuildingFootprintDataset(NonGeoDataset):
@@ -22,7 +23,9 @@ class BuildingFootprintDataset(NonGeoDataset):
22
23
  Using NonGeoDataset to avoid spatial indexing issues.
23
24
  """
24
25
 
25
- def __init__(self, raster_path, chip_size=(512, 512), transforms=None):
26
+ def __init__(
27
+ self, raster_path, chip_size=(512, 512), transforms=None, verbose=False
28
+ ):
26
29
  """
27
30
  Initialize the dataset.
28
31
 
@@ -30,6 +33,7 @@ class BuildingFootprintDataset(NonGeoDataset):
30
33
  raster_path: Path to the input raster file
31
34
  chip_size: Size of image chips to extract (height, width)
32
35
  transforms: Transforms to apply to the image
36
+ verbose: Whether to print detailed processing information
33
37
  """
34
38
  super().__init__()
35
39
 
@@ -37,6 +41,10 @@ class BuildingFootprintDataset(NonGeoDataset):
37
41
  self.raster_path = raster_path
38
42
  self.chip_size = chip_size
39
43
  self.transforms = transforms
44
+ self.verbose = verbose
45
+
46
+ # For tracking warnings about multi-band images
47
+ self.warned_about_bands = False
40
48
 
41
49
  # Open raster and get metadata
42
50
  with rasterio.open(self.raster_path) as src:
@@ -54,15 +62,21 @@ class BuildingFootprintDataset(NonGeoDataset):
54
62
  self.roi = box(*self.bounds)
55
63
 
56
64
  # Calculate number of chips in each dimension
57
- self.rows = self.height // self.chip_size[0]
58
- self.cols = self.width // self.chip_size[1]
65
+ # Use ceil division to ensure we cover the entire image
66
+ self.rows = (self.height + self.chip_size[0] - 1) // self.chip_size[0]
67
+ self.cols = (self.width + self.chip_size[1] - 1) // self.chip_size[1]
59
68
 
60
69
  print(
61
70
  f"Dataset initialized with {self.rows} rows and {self.cols} columns of chips"
62
71
  )
72
+ print(f"Image dimensions: {self.width} x {self.height} pixels")
73
+ print(f"Chip size: {self.chip_size[1]} x {self.chip_size[0]} pixels")
63
74
  if src.crs:
64
75
  print(f"CRS: {src.crs}")
65
76
 
77
+ # get raster stats
78
+ self.raster_stats = get_raster_stats(raster_path, divide_by=255)
79
+
66
80
  def __getitem__(self, idx):
67
81
  """
68
82
  Get an image chip from the dataset by index.
@@ -92,11 +106,17 @@ class BuildingFootprintDataset(NonGeoDataset):
92
106
 
93
107
  # Handle RGBA or multispectral images - keep only first 3 bands
94
108
  if image.shape[0] > 3:
95
- print(f"Image has {image.shape[0]} bands, using first 3 bands only")
109
+ if not self.warned_about_bands and self.verbose:
110
+ print(f"Image has {image.shape[0]} bands, using first 3 bands only")
111
+ self.warned_about_bands = True
96
112
  image = image[:3]
97
113
  elif image.shape[0] < 3:
98
114
  # If image has fewer than 3 bands, duplicate the last band to make 3
99
- print(f"Image has {image.shape[0]} bands, duplicating bands to make 3")
115
+ if not self.warned_about_bands and self.verbose:
116
+ print(
117
+ f"Image has {image.shape[0]} bands, duplicating bands to make 3"
118
+ )
119
+ self.warned_about_bands = True
100
120
  temp = np.zeros((3, image.shape[1], image.shape[2]), dtype=image.dtype)
101
121
  for c in range(3):
102
122
  temp[c] = image[min(c, image.shape[0] - 1)]
@@ -198,7 +218,7 @@ class BuildingFootprintExtractor:
198
218
 
199
219
  # Define the repository ID and model filename
200
220
  repo_id = "giswqs/geoai" # Update with your actual username/repo
201
- filename = "usa_building_footprints.pth"
221
+ filename = "building_footprints_usa.pth"
202
222
 
203
223
  # Ensure cache directory exists
204
224
  # cache_dir = os.path.join(
@@ -387,8 +407,279 @@ class BuildingFootprintExtractor:
387
407
 
388
408
  return gdf.iloc[keep_indices]
389
409
 
410
+ def filter_edge_buildings(self, gdf, raster_path, edge_buffer=10):
411
+ """
412
+ Filter out building detections that fall in padding/edge areas of the image.
413
+
414
+ Args:
415
+ gdf: GeoDataFrame with building footprint detections
416
+ raster_path: Path to the original raster file
417
+ edge_buffer: Buffer in pixels to consider as edge region
418
+
419
+ Returns:
420
+ GeoDataFrame with filtered building footprints
421
+ """
422
+ import rasterio
423
+ from shapely.geometry import box
424
+
425
+ # If no buildings detected, return empty GeoDataFrame
426
+ if gdf is None or len(gdf) == 0:
427
+ return gdf
428
+
429
+ print(f"Buildings before filtering: {len(gdf)}")
430
+
431
+ with rasterio.open(raster_path) as src:
432
+ # Get raster bounds
433
+ raster_bounds = src.bounds
434
+ raster_width = src.width
435
+ raster_height = src.height
436
+
437
+ # Convert edge buffer from pixels to geographic units
438
+ # We need the smallest dimension of a pixel in geographic units
439
+ pixel_width = (raster_bounds[2] - raster_bounds[0]) / raster_width
440
+ pixel_height = (raster_bounds[3] - raster_bounds[1]) / raster_height
441
+ buffer_size = min(pixel_width, pixel_height) * edge_buffer
442
+
443
+ # Create a slightly smaller bounding box to exclude edge regions
444
+ inner_bounds = (
445
+ raster_bounds[0] + buffer_size, # min x (west)
446
+ raster_bounds[1] + buffer_size, # min y (south)
447
+ raster_bounds[2] - buffer_size, # max x (east)
448
+ raster_bounds[3] - buffer_size, # max y (north)
449
+ )
450
+
451
+ # Check that inner bounds are valid
452
+ if inner_bounds[0] >= inner_bounds[2] or inner_bounds[1] >= inner_bounds[3]:
453
+ print("Warning: Edge buffer too large, using original bounds")
454
+ inner_box = box(*raster_bounds)
455
+ else:
456
+ inner_box = box(*inner_bounds)
457
+
458
+ # Filter out buildings that intersect with the edge of the image
459
+ filtered_gdf = gdf[gdf.intersects(inner_box)]
460
+
461
+ # Additional check for buildings that have >50% of their area outside the valid region
462
+ valid_buildings = []
463
+ for idx, row in filtered_gdf.iterrows():
464
+ if row.geometry.intersection(inner_box).area >= 0.5 * row.geometry.area:
465
+ valid_buildings.append(idx)
466
+
467
+ filtered_gdf = filtered_gdf.loc[valid_buildings]
468
+
469
+ print(f"Buildings after filtering: {len(filtered_gdf)}")
470
+
471
+ return filtered_gdf
472
+
473
+ def masks_to_vector(
474
+ self,
475
+ mask_path,
476
+ output_path=None,
477
+ simplify_tolerance=None,
478
+ mask_threshold=None,
479
+ small_building_area=None,
480
+ nms_iou_threshold=None,
481
+ regularize=True,
482
+ angle_threshold=15,
483
+ rectangularity_threshold=0.7,
484
+ ):
485
+ """
486
+ Convert a building mask GeoTIFF to vector polygons and save as GeoJSON.
487
+
488
+ Args:
489
+ mask_path: Path to the building masks GeoTIFF
490
+ output_path: Path to save the output GeoJSON (default: mask_path with .geojson extension)
491
+ simplify_tolerance: Tolerance for polygon simplification (default: self.simplify_tolerance)
492
+ mask_threshold: Threshold for mask binarization (default: self.mask_threshold)
493
+ small_building_area: Minimum area in pixels to keep a building (default: self.small_building_area)
494
+ nms_iou_threshold: IoU threshold for non-maximum suppression (default: self.nms_iou_threshold)
495
+ regularize: Whether to regularize buildings to right angles (default: True)
496
+ angle_threshold: Maximum deviation from 90 degrees for regularization (default: 15)
497
+ rectangularity_threshold: Threshold for rectangle simplification (default: 0.7)
498
+
499
+ Returns:
500
+ GeoDataFrame with building footprints
501
+ """
502
+ # Use class defaults if parameters not provided
503
+ simplify_tolerance = (
504
+ simplify_tolerance
505
+ if simplify_tolerance is not None
506
+ else self.simplify_tolerance
507
+ )
508
+ mask_threshold = (
509
+ mask_threshold if mask_threshold is not None else self.mask_threshold
510
+ )
511
+ small_building_area = (
512
+ small_building_area
513
+ if small_building_area is not None
514
+ else self.small_building_area
515
+ )
516
+ nms_iou_threshold = (
517
+ nms_iou_threshold
518
+ if nms_iou_threshold is not None
519
+ else self.nms_iou_threshold
520
+ )
521
+
522
+ # Set default output path if not provided
523
+ # if output_path is None:
524
+ # output_path = os.path.splitext(mask_path)[0] + ".geojson"
525
+
526
+ print(f"Converting mask to GeoJSON with parameters:")
527
+ print(f"- Mask threshold: {mask_threshold}")
528
+ print(f"- Min building area: {small_building_area}")
529
+ print(f"- Simplify tolerance: {simplify_tolerance}")
530
+ print(f"- NMS IoU threshold: {nms_iou_threshold}")
531
+ print(f"- Regularize buildings: {regularize}")
532
+ if regularize:
533
+ print(f"- Angle threshold: {angle_threshold}° from 90°")
534
+ print(f"- Rectangularity threshold: {rectangularity_threshold*100}%")
535
+
536
+ # Open the mask raster
537
+ with rasterio.open(mask_path) as src:
538
+ # Read the mask data
539
+ mask_data = src.read(1)
540
+ transform = src.transform
541
+ crs = src.crs
542
+
543
+ # Print mask statistics
544
+ print(f"Mask dimensions: {mask_data.shape}")
545
+ print(f"Mask value range: {mask_data.min()} to {mask_data.max()}")
546
+
547
+ # Prepare for connected component analysis
548
+ # Binarize the mask based on threshold
549
+ binary_mask = (mask_data > (mask_threshold * 255)).astype(np.uint8)
550
+
551
+ # Apply morphological operations for better results (optional)
552
+ kernel = np.ones((3, 3), np.uint8)
553
+ binary_mask = cv2.morphologyEx(binary_mask, cv2.MORPH_CLOSE, kernel)
554
+
555
+ # Find connected components
556
+ num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(
557
+ binary_mask, connectivity=8
558
+ )
559
+
560
+ print(
561
+ f"Found {num_labels-1} potential buildings"
562
+ ) # Subtract 1 for background
563
+
564
+ # Create list to store polygons and confidence values
565
+ all_polygons = []
566
+ all_confidences = []
567
+
568
+ # Process each component (skip the first one which is background)
569
+ for i in tqdm(range(1, num_labels)):
570
+ # Extract this building
571
+ area = stats[i, cv2.CC_STAT_AREA]
572
+
573
+ # Skip if too small
574
+ if area < small_building_area:
575
+ continue
576
+
577
+ # Create a mask for this building
578
+ building_mask = (labels == i).astype(np.uint8)
579
+
580
+ # Find contours
581
+ contours, _ = cv2.findContours(
582
+ building_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
583
+ )
584
+
585
+ # Process each contour
586
+ for contour in contours:
587
+ # Skip if too few points
588
+ if contour.shape[0] < 3:
589
+ continue
590
+
591
+ # Simplify contour if it has many points
592
+ if contour.shape[0] > 50 and simplify_tolerance > 0:
593
+ epsilon = simplify_tolerance * cv2.arcLength(contour, True)
594
+ contour = cv2.approxPolyDP(contour, epsilon, True)
595
+
596
+ # Convert to list of (x, y) coordinates
597
+ polygon_points = contour.reshape(-1, 2)
598
+
599
+ # Convert pixel coordinates to geographic coordinates
600
+ geo_points = []
601
+ for x, y in polygon_points:
602
+ gx, gy = transform * (x, y)
603
+ geo_points.append((gx, gy))
604
+
605
+ # Create Shapely polygon
606
+ if len(geo_points) >= 3:
607
+ try:
608
+ shapely_poly = Polygon(geo_points)
609
+ if shapely_poly.is_valid and shapely_poly.area > 0:
610
+ all_polygons.append(shapely_poly)
611
+
612
+ # Calculate "confidence" as normalized size
613
+ # This is a proxy since we don't have model confidence scores
614
+ normalized_size = min(1.0, area / 1000) # Cap at 1.0
615
+ all_confidences.append(normalized_size)
616
+ except Exception as e:
617
+ print(f"Error creating polygon: {e}")
618
+
619
+ print(f"Created {len(all_polygons)} valid polygons")
620
+
621
+ # Create GeoDataFrame
622
+ if not all_polygons:
623
+ print("No valid polygons found")
624
+ return None
625
+
626
+ gdf = gpd.GeoDataFrame(
627
+ {
628
+ "geometry": all_polygons,
629
+ "confidence": all_confidences,
630
+ "class": 1, # Building class
631
+ },
632
+ crs=crs,
633
+ )
634
+
635
+ # Apply non-maximum suppression to remove overlapping polygons
636
+ gdf = self._filter_overlapping_polygons(
637
+ gdf, nms_iou_threshold=nms_iou_threshold
638
+ )
639
+
640
+ print(f"Building count after NMS filtering: {len(gdf)}")
641
+
642
+ # Apply regularization if requested
643
+ if regularize and len(gdf) > 0:
644
+ # Convert pixel area to geographic units for min_area parameter
645
+ # Estimate pixel size in geographic units
646
+ with rasterio.open(mask_path) as src:
647
+ pixel_size_x = src.transform[
648
+ 0
649
+ ] # width of a pixel in geographic units
650
+ pixel_size_y = abs(
651
+ src.transform[4]
652
+ ) # height of a pixel in geographic units
653
+ avg_pixel_area = pixel_size_x * pixel_size_y
654
+
655
+ # Use 10 pixels as minimum area in geographic units
656
+ min_geo_area = 10 * avg_pixel_area
657
+
658
+ # Regularize buildings
659
+ gdf = self.regularize_buildings(
660
+ gdf,
661
+ min_area=min_geo_area,
662
+ angle_threshold=angle_threshold,
663
+ rectangularity_threshold=rectangularity_threshold,
664
+ )
665
+
666
+ # Save to file
667
+ if output_path:
668
+ gdf.to_file(output_path)
669
+ print(f"Saved {len(gdf)} building footprints to {output_path}")
670
+
671
+ return gdf
672
+
390
673
  @torch.no_grad()
391
- def process_raster(self, raster_path, output_path=None, batch_size=4, **kwargs):
674
+ def process_raster(
675
+ self,
676
+ raster_path,
677
+ output_path=None,
678
+ batch_size=4,
679
+ filter_edges=True,
680
+ edge_buffer=20,
681
+ **kwargs,
682
+ ):
392
683
  """
393
684
  Process a raster file to extract building footprints with customizable parameters.
394
685
 
@@ -396,6 +687,8 @@ class BuildingFootprintExtractor:
396
687
  raster_path: Path to input raster file
397
688
  output_path: Path to output GeoJSON file (optional)
398
689
  batch_size: Batch size for processing
690
+ filter_edges: Whether to filter out buildings at the edges of the image
691
+ edge_buffer: Size of edge buffer in pixels to filter out buildings (if filter_edges=True)
399
692
  **kwargs: Additional parameters:
400
693
  confidence_threshold: Minimum confidence score to keep a detection (0.0-1.0)
401
694
  overlap: Overlap between adjacent tiles (0.0-1.0)
@@ -430,9 +723,13 @@ class BuildingFootprintExtractor:
430
723
  print(f"- Mask threshold: {mask_threshold}")
431
724
  print(f"- Min building area: {small_building_area}")
432
725
  print(f"- Simplify tolerance: {simplify_tolerance}")
726
+ print(f"- Filter edge buildings: {filter_edges}")
727
+ if filter_edges:
728
+ print(f"- Edge buffer size: {edge_buffer} pixels")
433
729
 
434
730
  # Create dataset
435
731
  dataset = BuildingFootprintDataset(raster_path=raster_path, chip_size=chip_size)
732
+ self.raster_stats = dataset.raster_stats
436
733
 
437
734
  # Custom collate function to handle Shapely objects
438
735
  def custom_collate(batch):
@@ -603,6 +900,10 @@ class BuildingFootprintExtractor:
603
900
  gdf, nms_iou_threshold=nms_iou_threshold
604
901
  )
605
902
 
903
+ # Filter edge buildings if requested
904
+ if filter_edges:
905
+ gdf = self.filter_edge_buildings(gdf, raster_path, edge_buffer=edge_buffer)
906
+
606
907
  # Save to file if requested
607
908
  if output_path:
608
909
  gdf.to_file(output_path, driver="GeoJSON")
@@ -610,22 +911,473 @@ class BuildingFootprintExtractor:
610
911
 
611
912
  return gdf
612
913
 
914
+ def save_masks_as_geotiff(
915
+ self, raster_path, output_path=None, batch_size=4, verbose=False, **kwargs
916
+ ):
917
+ """
918
+ Process a raster file to extract building footprint masks and save as GeoTIFF.
919
+
920
+ Args:
921
+ raster_path: Path to input raster file
922
+ output_path: Path to output GeoTIFF file (optional, default: input_masks.tif)
923
+ batch_size: Batch size for processing
924
+ verbose: Whether to print detailed processing information
925
+ **kwargs: Additional parameters:
926
+ confidence_threshold: Minimum confidence score to keep a detection (0.0-1.0)
927
+ chip_size: Size of image chips for processing (height, width)
928
+ mask_threshold: Threshold for mask binarization (0.0-1.0)
929
+
930
+ Returns:
931
+ Path to the saved GeoTIFF file
932
+ """
933
+
934
+ # Get parameters from kwargs or use instance defaults
935
+ confidence_threshold = kwargs.get(
936
+ "confidence_threshold", self.confidence_threshold
937
+ )
938
+ chip_size = kwargs.get("chip_size", self.chip_size)
939
+ mask_threshold = kwargs.get("mask_threshold", self.mask_threshold)
940
+
941
+ # Set default output path if not provided
942
+ if output_path is None:
943
+ output_path = os.path.splitext(raster_path)[0] + "_masks.tif"
944
+
945
+ # Print parameters being used
946
+ print(f"Processing masks with parameters:")
947
+ print(f"- Confidence threshold: {confidence_threshold}")
948
+ print(f"- Chip size: {chip_size}")
949
+ print(f"- Mask threshold: {mask_threshold}")
950
+
951
+ # Create dataset
952
+ dataset = BuildingFootprintDataset(
953
+ raster_path=raster_path, chip_size=chip_size, verbose=verbose
954
+ )
955
+
956
+ # Store a flag to avoid repetitive messages
957
+ self.raster_stats = dataset.raster_stats
958
+ seen_warnings = {
959
+ "bands": False,
960
+ "resize": {}, # Dictionary to track resize warnings by shape
961
+ }
962
+
963
+ # Open original raster to get metadata
964
+ with rasterio.open(raster_path) as src:
965
+ # Create output binary mask raster with same dimensions as input
966
+ output_profile = src.profile.copy()
967
+ output_profile.update(
968
+ dtype=rasterio.uint8,
969
+ count=1, # Single band for building mask
970
+ compress="lzw",
971
+ nodata=0,
972
+ )
973
+
974
+ # Create output mask raster
975
+ with rasterio.open(output_path, "w", **output_profile) as dst:
976
+ # Initialize mask with zeros
977
+ mask_array = np.zeros((src.height, src.width), dtype=np.uint8)
978
+
979
+ # Custom collate function to handle Shapely objects
980
+ def custom_collate(batch):
981
+ """Custom collate function for DataLoader"""
982
+ elem = batch[0]
983
+ if isinstance(elem, dict):
984
+ result = {}
985
+ for key in elem:
986
+ if key == "bbox":
987
+ # Don't collate shapely objects, keep as list
988
+ result[key] = [d[key] for d in batch]
989
+ else:
990
+ # For tensors and other collatable types
991
+ try:
992
+ result[key] = (
993
+ torch.utils.data._utils.collate.default_collate(
994
+ [d[key] for d in batch]
995
+ )
996
+ )
997
+ except TypeError:
998
+ # Fall back to list for non-collatable types
999
+ result[key] = [d[key] for d in batch]
1000
+ return result
1001
+ else:
1002
+ # Default collate for non-dict types
1003
+ return torch.utils.data._utils.collate.default_collate(batch)
1004
+
1005
+ # Create dataloader
1006
+ dataloader = torch.utils.data.DataLoader(
1007
+ dataset,
1008
+ batch_size=batch_size,
1009
+ shuffle=False,
1010
+ num_workers=0,
1011
+ collate_fn=custom_collate,
1012
+ )
1013
+
1014
+ # Process batches
1015
+ print(f"Processing raster with {len(dataloader)} batches")
1016
+ for batch in tqdm(dataloader):
1017
+ # Move images to device
1018
+ images = batch["image"].to(self.device)
1019
+ coords = batch["coords"] # (i, j) coordinates in pixels
1020
+
1021
+ # Run inference
1022
+ with torch.no_grad():
1023
+ predictions = self.model(images)
1024
+
1025
+ # Process predictions
1026
+ for idx, prediction in enumerate(predictions):
1027
+ masks = prediction["masks"].cpu().numpy()
1028
+ scores = prediction["scores"].cpu().numpy()
1029
+
1030
+ # Skip if no predictions
1031
+ if len(scores) == 0:
1032
+ continue
1033
+
1034
+ # Filter by confidence threshold
1035
+ valid_indices = scores >= confidence_threshold
1036
+ masks = masks[valid_indices]
1037
+ scores = scores[valid_indices]
1038
+
1039
+ # Skip if no valid predictions
1040
+ if len(scores) == 0:
1041
+ continue
1042
+
1043
+ # Get window coordinates
1044
+ if isinstance(coords, list):
1045
+ coord_item = coords[idx]
1046
+ if isinstance(coord_item, tuple) and len(coord_item) == 2:
1047
+ i, j = coord_item
1048
+ elif isinstance(coord_item, torch.Tensor):
1049
+ i, j = coord_item.cpu().numpy().tolist()
1050
+ else:
1051
+ print(f"Unexpected coords format: {type(coord_item)}")
1052
+ continue
1053
+ elif isinstance(coords, torch.Tensor):
1054
+ i, j = coords[idx].cpu().numpy().tolist()
1055
+ else:
1056
+ print(f"Unexpected coords type: {type(coords)}")
1057
+ continue
1058
+
1059
+ # Get window size
1060
+ if isinstance(batch["window_size"], list):
1061
+ window_item = batch["window_size"][idx]
1062
+ if isinstance(window_item, tuple) and len(window_item) == 2:
1063
+ window_width, window_height = window_item
1064
+ elif isinstance(window_item, torch.Tensor):
1065
+ window_width, window_height = (
1066
+ window_item.cpu().numpy().tolist()
1067
+ )
1068
+ else:
1069
+ print(
1070
+ f"Unexpected window_size format: {type(window_item)}"
1071
+ )
1072
+ continue
1073
+ elif isinstance(batch["window_size"], torch.Tensor):
1074
+ window_width, window_height = (
1075
+ batch["window_size"][idx].cpu().numpy().tolist()
1076
+ )
1077
+ else:
1078
+ print(
1079
+ f"Unexpected window_size type: {type(batch['window_size'])}"
1080
+ )
1081
+ continue
1082
+
1083
+ # Combine all masks for this window
1084
+ combined_mask = np.zeros(
1085
+ (window_height, window_width), dtype=np.uint8
1086
+ )
1087
+
1088
+ for mask in masks:
1089
+ # Get the binary mask
1090
+ binary_mask = (mask[0] > mask_threshold).astype(
1091
+ np.uint8
1092
+ ) * 255
1093
+
1094
+ # Handle size mismatch - resize binary_mask if needed
1095
+ mask_h, mask_w = binary_mask.shape
1096
+ if mask_h != window_height or mask_w != window_width:
1097
+ resize_key = f"{(mask_h, mask_w)}->{(window_height, window_width)}"
1098
+ if resize_key not in seen_warnings["resize"]:
1099
+ if verbose:
1100
+ print(
1101
+ f"Resizing mask from {binary_mask.shape} to {(window_height, window_width)}"
1102
+ )
1103
+ else:
1104
+ if not seen_warnings[
1105
+ "resize"
1106
+ ]: # If this is the first resize warning
1107
+ print(
1108
+ f"Resizing masks at image edges (set verbose=True for details)"
1109
+ )
1110
+ seen_warnings["resize"][resize_key] = True
1111
+
1112
+ # Crop or pad the binary mask to match window size
1113
+ resized_mask = np.zeros(
1114
+ (window_height, window_width), dtype=np.uint8
1115
+ )
1116
+ copy_h = min(mask_h, window_height)
1117
+ copy_w = min(mask_w, window_width)
1118
+ resized_mask[:copy_h, :copy_w] = binary_mask[
1119
+ :copy_h, :copy_w
1120
+ ]
1121
+ binary_mask = resized_mask
1122
+
1123
+ # Update combined mask (taking maximum where masks overlap)
1124
+ combined_mask = np.maximum(combined_mask, binary_mask)
1125
+
1126
+ # Write combined mask to output array
1127
+ # Handle edge cases where window might be smaller than chip size
1128
+ h, w = combined_mask.shape
1129
+ valid_h = min(h, src.height - j)
1130
+ valid_w = min(w, src.width - i)
1131
+
1132
+ if valid_h > 0 and valid_w > 0:
1133
+ mask_array[j : j + valid_h, i : i + valid_w] = np.maximum(
1134
+ mask_array[j : j + valid_h, i : i + valid_w],
1135
+ combined_mask[:valid_h, :valid_w],
1136
+ )
1137
+
1138
+ # Write the final mask to the output file
1139
+ dst.write(mask_array, 1)
1140
+
1141
+ print(f"Building masks saved to {output_path}")
1142
+ return output_path
1143
+
1144
+ def regularize_buildings(
1145
+ self,
1146
+ gdf,
1147
+ min_area=10,
1148
+ angle_threshold=15,
1149
+ orthogonality_threshold=0.3,
1150
+ rectangularity_threshold=0.7,
1151
+ ):
1152
+ """
1153
+ Regularize building footprints to enforce right angles and rectangular shapes.
1154
+
1155
+ Args:
1156
+ gdf: GeoDataFrame with building footprints
1157
+ min_area: Minimum area in square units to keep a building
1158
+ angle_threshold: Maximum deviation from 90 degrees to consider an angle as orthogonal (degrees)
1159
+ orthogonality_threshold: Percentage of angles that must be orthogonal for a building to be regularized
1160
+ rectangularity_threshold: Minimum area ratio to building's oriented bounding box for rectangular simplification
1161
+
1162
+ Returns:
1163
+ GeoDataFrame with regularized building footprints
1164
+ """
1165
+ import numpy as np
1166
+ from shapely.geometry import Polygon, MultiPolygon, box
1167
+ from shapely.affinity import rotate, translate
1168
+ import geopandas as gpd
1169
+ import math
1170
+ from tqdm import tqdm
1171
+ import cv2
1172
+
1173
+ def get_angle(p1, p2, p3):
1174
+ """Calculate angle between three points in degrees (0-180)"""
1175
+ a = np.array(p1)
1176
+ b = np.array(p2)
1177
+ c = np.array(p3)
1178
+
1179
+ ba = a - b
1180
+ bc = c - b
1181
+
1182
+ cosine_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
1183
+ # Handle numerical errors that could push cosine outside [-1, 1]
1184
+ cosine_angle = np.clip(cosine_angle, -1.0, 1.0)
1185
+ angle = np.degrees(np.arccos(cosine_angle))
1186
+
1187
+ return angle
1188
+
1189
+ def is_orthogonal(angle, threshold=angle_threshold):
1190
+ """Check if angle is close to 90 degrees"""
1191
+ return abs(angle - 90) <= threshold
1192
+
1193
+ def calculate_dominant_direction(polygon):
1194
+ """Find the dominant direction of a polygon using PCA"""
1195
+ # Extract coordinates
1196
+ coords = np.array(polygon.exterior.coords)
1197
+
1198
+ # Mean center the coordinates
1199
+ mean = np.mean(coords, axis=0)
1200
+ centered_coords = coords - mean
1201
+
1202
+ # Calculate covariance matrix and its eigenvalues/eigenvectors
1203
+ cov_matrix = np.cov(centered_coords.T)
1204
+ eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
1205
+
1206
+ # Get the index of the largest eigenvalue
1207
+ largest_idx = np.argmax(eigenvalues)
1208
+
1209
+ # Get the corresponding eigenvector (principal axis)
1210
+ principal_axis = eigenvectors[:, largest_idx]
1211
+
1212
+ # Calculate the angle in degrees
1213
+ angle_rad = np.arctan2(principal_axis[1], principal_axis[0])
1214
+ angle_deg = np.degrees(angle_rad)
1215
+
1216
+ # Normalize to range 0-180
1217
+ if angle_deg < 0:
1218
+ angle_deg += 180
1219
+
1220
+ return angle_deg
1221
+
1222
+ def create_oriented_envelope(polygon, angle_deg):
1223
+ """Create an oriented minimum area rectangle for the polygon"""
1224
+ # Create a rotated rectangle using OpenCV method (more robust than Shapely methods)
1225
+ coords = np.array(polygon.exterior.coords)[:-1].astype(
1226
+ np.float32
1227
+ ) # Skip the last point (same as first)
1228
+
1229
+ # Use OpenCV's minAreaRect
1230
+ rect = cv2.minAreaRect(coords)
1231
+ box_points = cv2.boxPoints(rect)
1232
+
1233
+ # Convert to shapely polygon
1234
+ oriented_box = Polygon(box_points)
1235
+
1236
+ return oriented_box
1237
+
1238
+ def get_rectangularity(polygon, oriented_box):
1239
+ """Calculate the rectangularity (area ratio to its oriented bounding box)"""
1240
+ if oriented_box.area == 0:
1241
+ return 0
1242
+ return polygon.area / oriented_box.area
1243
+
1244
+ def check_orthogonality(polygon):
1245
+ """Check what percentage of angles in the polygon are orthogonal"""
1246
+ coords = list(polygon.exterior.coords)
1247
+ if len(coords) <= 4: # Triangle or point
1248
+ return 0
1249
+
1250
+ # Remove last point (same as first)
1251
+ coords = coords[:-1]
1252
+
1253
+ orthogonal_count = 0
1254
+ total_angles = len(coords)
1255
+
1256
+ for i in range(total_angles):
1257
+ p1 = coords[i]
1258
+ p2 = coords[(i + 1) % total_angles]
1259
+ p3 = coords[(i + 2) % total_angles]
1260
+
1261
+ angle = get_angle(p1, p2, p3)
1262
+ if is_orthogonal(angle):
1263
+ orthogonal_count += 1
1264
+
1265
+ return orthogonal_count / total_angles
1266
+
1267
+ def simplify_to_rectangle(polygon):
1268
+ """Simplify a polygon to a rectangle using its oriented bounding box"""
1269
+ # Get dominant direction
1270
+ angle = calculate_dominant_direction(polygon)
1271
+
1272
+ # Create oriented envelope
1273
+ rect = create_oriented_envelope(polygon, angle)
1274
+
1275
+ return rect
1276
+
1277
+ if gdf is None or len(gdf) == 0:
1278
+ print("No buildings to regularize")
1279
+ return gdf
1280
+
1281
+ print(f"Regularizing {len(gdf)} building footprints...")
1282
+ print(f"- Angle threshold: {angle_threshold}° from 90°")
1283
+ print(f"- Min orthogonality: {orthogonality_threshold*100}% of angles")
1284
+ print(
1285
+ f"- Min rectangularity: {rectangularity_threshold*100}% of bounding box area"
1286
+ )
1287
+
1288
+ # Create a copy to avoid modifying the original
1289
+ result_gdf = gdf.copy()
1290
+
1291
+ # Track statistics
1292
+ total_buildings = len(gdf)
1293
+ regularized_count = 0
1294
+ rectangularized_count = 0
1295
+
1296
+ # Process each building
1297
+ for idx, row in tqdm(gdf.iterrows(), total=len(gdf)):
1298
+ geom = row.geometry
1299
+
1300
+ # Skip invalid or empty geometries
1301
+ if geom is None or geom.is_empty:
1302
+ continue
1303
+
1304
+ # Handle MultiPolygons by processing the largest part
1305
+ if isinstance(geom, MultiPolygon):
1306
+ areas = [p.area for p in geom.geoms]
1307
+ if not areas:
1308
+ continue
1309
+ geom = list(geom.geoms)[np.argmax(areas)]
1310
+
1311
+ # Filter out tiny buildings
1312
+ if geom.area < min_area:
1313
+ continue
1314
+
1315
+ # Check orthogonality
1316
+ orthogonality = check_orthogonality(geom)
1317
+
1318
+ # Create oriented envelope
1319
+ oriented_box = create_oriented_envelope(
1320
+ geom, calculate_dominant_direction(geom)
1321
+ )
1322
+
1323
+ # Check rectangularity
1324
+ rectangularity = get_rectangularity(geom, oriented_box)
1325
+
1326
+ # Decide how to regularize
1327
+ if rectangularity >= rectangularity_threshold:
1328
+ # Building is already quite rectangular, simplify to a rectangle
1329
+ result_gdf.at[idx, "geometry"] = oriented_box
1330
+ result_gdf.at[idx, "regularized"] = "rectangle"
1331
+ rectangularized_count += 1
1332
+ elif orthogonality >= orthogonality_threshold:
1333
+ # Building has many orthogonal angles but isn't rectangular
1334
+ # Could implement more sophisticated regularization here
1335
+ # For now, we'll still use the oriented rectangle
1336
+ result_gdf.at[idx, "geometry"] = oriented_box
1337
+ result_gdf.at[idx, "regularized"] = "orthogonal"
1338
+ regularized_count += 1
1339
+ else:
1340
+ # Building doesn't have clear orthogonal structure
1341
+ # Keep original but flag as unmodified
1342
+ result_gdf.at[idx, "regularized"] = "original"
1343
+
1344
+ # Report statistics
1345
+ print(f"Regularization completed:")
1346
+ print(f"- Total buildings: {total_buildings}")
1347
+ print(
1348
+ f"- Rectangular buildings: {rectangularized_count} ({rectangularized_count/total_buildings*100:.1f}%)"
1349
+ )
1350
+ print(
1351
+ f"- Other regularized buildings: {regularized_count} ({regularized_count/total_buildings*100:.1f}%)"
1352
+ )
1353
+ print(
1354
+ f"- Unmodified buildings: {total_buildings-rectangularized_count-regularized_count} ({(total_buildings-rectangularized_count-regularized_count)/total_buildings*100:.1f}%)"
1355
+ )
1356
+
1357
+ return result_gdf
1358
+
613
1359
  def visualize_results(
614
1360
  self, raster_path, gdf=None, output_path=None, figsize=(12, 12)
615
1361
  ):
616
1362
  """
617
- Visualize building detection results.
1363
+ Visualize building detection results with proper coordinate transformation.
1364
+
1365
+ This function displays building footprints on top of the raster image,
1366
+ ensuring proper alignment between the GeoDataFrame polygons and the image.
618
1367
 
619
1368
  Args:
620
1369
  raster_path: Path to input raster
621
1370
  gdf: GeoDataFrame with building polygons (optional)
622
1371
  output_path: Path to save visualization (optional)
623
1372
  figsize: Figure size (width, height) in inches
1373
+
1374
+ Returns:
1375
+ bool: True if visualization was successful
624
1376
  """
625
1377
  # Check if raster file exists
626
1378
  if not os.path.exists(raster_path):
627
1379
  print(f"Error: Raster file '{raster_path}' not found.")
628
- return
1380
+ return False
629
1381
 
630
1382
  # Process raster if GeoDataFrame not provided
631
1383
  if gdf is None:
@@ -633,7 +1385,26 @@ class BuildingFootprintExtractor:
633
1385
 
634
1386
  if gdf is None or len(gdf) == 0:
635
1387
  print("No buildings to visualize")
636
- return
1388
+ return False
1389
+
1390
+ # Check if confidence column exists in the GeoDataFrame
1391
+ has_confidence = False
1392
+ if hasattr(gdf, "columns") and "confidence" in gdf.columns:
1393
+ # Try to access a confidence value to confirm it works
1394
+ try:
1395
+ if len(gdf) > 0:
1396
+ # Try getitem access
1397
+ conf_val = gdf["confidence"].iloc[0]
1398
+ has_confidence = True
1399
+ print(
1400
+ f"Using confidence values (range: {gdf['confidence'].min():.2f} - {gdf['confidence'].max():.2f})"
1401
+ )
1402
+ except Exception as e:
1403
+ print(f"Confidence column exists but couldn't access values: {e}")
1404
+ has_confidence = False
1405
+ else:
1406
+ print("No confidence column found in GeoDataFrame")
1407
+ has_confidence = False
637
1408
 
638
1409
  # Read raster for visualization
639
1410
  with rasterio.open(raster_path) as src:
@@ -651,8 +1422,27 @@ class BuildingFootprintExtractor:
651
1422
  image = src.read(
652
1423
  out_shape=out_shape, resampling=rasterio.enums.Resampling.bilinear
653
1424
  )
1425
+
1426
+ # Create a scaled transform for the resampled image
1427
+ # Calculate scaling factors
1428
+ x_scale = src.width / out_shape[2]
1429
+ y_scale = src.height / out_shape[1]
1430
+
1431
+ # Get the original transform
1432
+ orig_transform = src.transform
1433
+
1434
+ # Create a scaled transform
1435
+ scaled_transform = rasterio.transform.Affine(
1436
+ orig_transform.a * x_scale,
1437
+ orig_transform.b,
1438
+ orig_transform.c,
1439
+ orig_transform.d,
1440
+ orig_transform.e * y_scale,
1441
+ orig_transform.f,
1442
+ )
654
1443
  else:
655
1444
  image = src.read()
1445
+ scaled_transform = src.transform
656
1446
 
657
1447
  # Convert to RGB for display
658
1448
  if image.shape[0] > 3:
@@ -671,60 +1461,134 @@ class BuildingFootprintExtractor:
671
1461
 
672
1462
  # Get image bounds
673
1463
  bounds = src.bounds
1464
+ crs = src.crs
674
1465
 
675
1466
  # Create figure with appropriate aspect ratio
676
1467
  aspect_ratio = image.shape[1] / image.shape[0] # width / height
677
1468
  plt.figure(figsize=(figsize[0], figsize[0] / aspect_ratio))
678
-
679
- # Create axis with the right projection if CRS is available
680
1469
  ax = plt.gca()
681
1470
 
682
1471
  # Display image
683
1472
  ax.imshow(image)
684
1473
 
685
- # Convert GeoDataFrame to pixel coordinates for plotting
686
- with rasterio.open(raster_path) as src:
1474
+ # Make sure the GeoDataFrame has the same CRS as the raster
1475
+ if gdf.crs != crs:
1476
+ print(f"Reprojecting GeoDataFrame from {gdf.crs} to {crs}")
1477
+ gdf = gdf.to_crs(crs)
1478
+
1479
+ # Set up colors for confidence visualization
1480
+ if has_confidence:
1481
+ try:
1482
+ import matplotlib.cm as cm
1483
+ from matplotlib.colors import Normalize
1484
+
1485
+ # Get min/max confidence values
1486
+ min_conf = gdf["confidence"].min()
1487
+ max_conf = gdf["confidence"].max()
687
1488
 
688
- def geo_to_pixel(x, y):
689
- return ~src.transform * (x, y)
1489
+ # Set up normalization and colormap
1490
+ norm = Normalize(vmin=min_conf, vmax=max_conf)
1491
+ cmap = cm.viridis
1492
+
1493
+ # Create scalar mappable for colorbar
1494
+ sm = cm.ScalarMappable(cmap=cmap, norm=norm)
1495
+ sm.set_array([])
1496
+
1497
+ # Add colorbar
1498
+ cbar = plt.colorbar(
1499
+ sm, ax=ax, orientation="vertical", shrink=0.7, pad=0.01
1500
+ )
1501
+ cbar.set_label("Confidence Score")
1502
+ except Exception as e:
1503
+ print(f"Error setting up confidence visualization: {e}")
1504
+ has_confidence = False
1505
+
1506
+ # Function to convert coordinates
1507
+ def geo_to_pixel(geometry, transform):
1508
+ """Convert geometry to pixel coordinates using the provided transform."""
1509
+ if geometry.is_empty:
1510
+ return None
1511
+
1512
+ if geometry.geom_type == "Polygon":
1513
+ # Get exterior coordinates
1514
+ exterior_coords = list(geometry.exterior.coords)
1515
+
1516
+ # Convert to pixel coordinates
1517
+ pixel_coords = [~transform * (x, y) for x, y in exterior_coords]
1518
+
1519
+ # Split into x and y lists
1520
+ pixel_x = [coord[0] for coord in pixel_coords]
1521
+ pixel_y = [coord[1] for coord in pixel_coords]
1522
+
1523
+ return pixel_x, pixel_y
1524
+ else:
1525
+ print(f"Unsupported geometry type: {geometry.geom_type}")
1526
+ return None
690
1527
 
691
- # Plot each building footprint
692
- for _, row in gdf.iterrows():
1528
+ # Plot each building footprint
1529
+ for idx, row in gdf.iterrows():
1530
+ try:
693
1531
  # Convert polygon to pixel coordinates
694
- geom = row.geometry
695
- if geom.is_empty:
696
- continue
1532
+ coords = geo_to_pixel(row.geometry, scaled_transform)
697
1533
 
698
- try:
699
- # Get polygon exterior coordinates
700
- x, y = geom.exterior.xy
1534
+ if coords:
1535
+ pixel_x, pixel_y = coords
701
1536
 
702
- # Convert to pixel coordinates
703
- pixel_coords = [geo_to_pixel(x[i], y[i]) for i in range(len(x))]
704
- pixel_x = [coord[0] for coord in pixel_coords]
705
- pixel_y = [coord[1] for coord in pixel_coords]
1537
+ if has_confidence:
1538
+ try:
1539
+ # Get confidence value using different methods
1540
+ # Method 1: Try direct attribute access
1541
+ confidence = None
1542
+ try:
1543
+ confidence = row.confidence
1544
+ except:
1545
+ pass
1546
+
1547
+ # Method 2: Try dictionary-style access
1548
+ if confidence is None:
1549
+ try:
1550
+ confidence = row["confidence"]
1551
+ except:
1552
+ pass
706
1553
 
707
- # Plot polygon
708
- ax.plot(pixel_x, pixel_y, color="red", linewidth=1)
709
- except Exception as e:
710
- print(f"Error plotting polygon: {e}")
1554
+ # Method 3: Try accessing by index from the GeoDataFrame
1555
+ if confidence is None:
1556
+ try:
1557
+ confidence = gdf.iloc[idx]["confidence"]
1558
+ except:
1559
+ pass
1560
+
1561
+ if confidence is not None:
1562
+ color = cmap(norm(confidence))
1563
+ # Fill polygon with semi-transparent color
1564
+ ax.fill(pixel_x, pixel_y, color=color, alpha=0.5)
1565
+ # Draw border
1566
+ ax.plot(
1567
+ pixel_x,
1568
+ pixel_y,
1569
+ color=color,
1570
+ linewidth=1,
1571
+ alpha=0.8,
1572
+ )
1573
+ else:
1574
+ # Fall back to red if confidence value couldn't be accessed
1575
+ ax.plot(pixel_x, pixel_y, color="red", linewidth=1)
1576
+ except Exception as e:
1577
+ print(
1578
+ f"Error using confidence value for polygon {idx}: {e}"
1579
+ )
1580
+ ax.plot(pixel_x, pixel_y, color="red", linewidth=1)
1581
+ else:
1582
+ # No confidence data, just plot outlines in red
1583
+ ax.plot(pixel_x, pixel_y, color="red", linewidth=1)
1584
+ except Exception as e:
1585
+ print(f"Error plotting polygon {idx}: {e}")
711
1586
 
712
1587
  # Remove axes
713
1588
  ax.set_xticks([])
714
1589
  ax.set_yticks([])
715
1590
  ax.set_title(f"Building Footprints (Found: {len(gdf)})")
716
1591
 
717
- # Add colorbar for confidence if available
718
- if "confidence" in gdf.columns:
719
- # Create a colorbar legend
720
- sm = plt.cm.ScalarMappable(
721
- cmap=plt.cm.viridis,
722
- norm=plt.Normalize(gdf.confidence.min(), gdf.confidence.max()),
723
- )
724
- sm.set_array([])
725
- cbar = plt.colorbar(sm, ax=ax, orientation="vertical", shrink=0.7)
726
- cbar.set_label("Confidence")
727
-
728
1592
  # Save if requested
729
1593
  if output_path:
730
1594
  plt.tight_layout()
@@ -734,14 +1598,12 @@ class BuildingFootprintExtractor:
734
1598
  plt.close()
735
1599
 
736
1600
  # Create a simpler visualization focused just on a subset of buildings
737
- # This helps when the raster is very large
738
- plt.figure(figsize=figsize)
739
- ax = plt.gca()
1601
+ if len(gdf) > 0:
1602
+ plt.figure(figsize=figsize)
1603
+ ax = plt.gca()
740
1604
 
741
- # Choose a subset of the image to show
742
- with rasterio.open(raster_path) as src:
743
- # Get a sample window based on the first few buildings
744
- if len(gdf) > 0:
1605
+ # Choose a subset of the image to show
1606
+ with rasterio.open(raster_path) as src:
745
1607
  # Get centroid of first building
746
1608
  sample_geom = gdf.iloc[0].geometry
747
1609
  centroid = sample_geom.centroid
@@ -776,36 +1638,109 @@ class BuildingFootprintExtractor:
776
1638
 
777
1639
  sample_image = np.clip(sample_image, 0, 1)
778
1640
 
779
- # Get transform for this window
780
- window_transform = src.window_transform(window)
781
-
782
1641
  # Display sample image
783
- ax.imshow(sample_image)
1642
+ ax.imshow(sample_image, extent=[0, window.width, window.height, 0])
784
1643
 
785
- # Filter buildings that intersect with this window
1644
+ # Get the correct transform for this window
1645
+ window_transform = src.window_transform(window)
1646
+
1647
+ # Calculate bounds of the window
786
1648
  window_bounds = rasterio.windows.bounds(window, src.transform)
787
1649
  window_box = box(*window_bounds)
1650
+
1651
+ # Filter buildings that intersect with this window
788
1652
  visible_gdf = gdf[gdf.intersects(window_box)]
789
1653
 
790
- # Plot building footprints in this view
791
- for _, row in visible_gdf.iterrows():
1654
+ # Set up colors for sample view if confidence data exists
1655
+ if has_confidence:
1656
+ try:
1657
+ # Reuse the same normalization and colormap from main view
1658
+ sample_sm = cm.ScalarMappable(cmap=cmap, norm=norm)
1659
+ sample_sm.set_array([])
1660
+
1661
+ # Add colorbar to sample view
1662
+ sample_cbar = plt.colorbar(
1663
+ sample_sm,
1664
+ ax=ax,
1665
+ orientation="vertical",
1666
+ shrink=0.7,
1667
+ pad=0.01,
1668
+ )
1669
+ sample_cbar.set_label("Confidence Score")
1670
+ except Exception as e:
1671
+ print(f"Error setting up sample confidence visualization: {e}")
1672
+
1673
+ # Plot building footprints in sample view
1674
+ for idx, row in visible_gdf.iterrows():
792
1675
  try:
793
- # Get polygon exterior coordinates
1676
+ # Get window-relative pixel coordinates
794
1677
  geom = row.geometry
1678
+
1679
+ # Skip empty geometries
795
1680
  if geom.is_empty:
796
1681
  continue
797
1682
 
798
- x, y = geom.exterior.xy
1683
+ # Get exterior coordinates
1684
+ exterior_coords = list(geom.exterior.coords)
1685
+
1686
+ # Convert to pixel coordinates relative to window origin
1687
+ pixel_coords = []
1688
+ for x, y in exterior_coords:
1689
+ px, py = ~src.transform * (x, y) # Convert to image pixels
1690
+ # Make coordinates relative to window
1691
+ px = px - window.col_off
1692
+ py = py - window.row_off
1693
+ pixel_coords.append((px, py))
799
1694
 
800
- # Convert to pixel coordinates relative to window
801
- pixel_coords = [
802
- ~window_transform * (x[i], y[i]) for i in range(len(x))
803
- ]
1695
+ # Extract x and y coordinates
804
1696
  pixel_x = [coord[0] for coord in pixel_coords]
805
1697
  pixel_y = [coord[1] for coord in pixel_coords]
806
1698
 
807
- # Plot polygon
808
- ax.plot(pixel_x, pixel_y, color="red", linewidth=1.5)
1699
+ # Use confidence colors if available
1700
+ if has_confidence:
1701
+ try:
1702
+ # Try different methods to access confidence
1703
+ confidence = None
1704
+ try:
1705
+ confidence = row.confidence
1706
+ except:
1707
+ pass
1708
+
1709
+ if confidence is None:
1710
+ try:
1711
+ confidence = row["confidence"]
1712
+ except:
1713
+ pass
1714
+
1715
+ if confidence is None:
1716
+ try:
1717
+ confidence = visible_gdf.iloc[idx]["confidence"]
1718
+ except:
1719
+ pass
1720
+
1721
+ if confidence is not None:
1722
+ color = cmap(norm(confidence))
1723
+ # Fill polygon with semi-transparent color
1724
+ ax.fill(pixel_x, pixel_y, color=color, alpha=0.5)
1725
+ # Draw border
1726
+ ax.plot(
1727
+ pixel_x,
1728
+ pixel_y,
1729
+ color=color,
1730
+ linewidth=1.5,
1731
+ alpha=0.8,
1732
+ )
1733
+ else:
1734
+ ax.plot(
1735
+ pixel_x, pixel_y, color="red", linewidth=1.5
1736
+ )
1737
+ except Exception as e:
1738
+ print(
1739
+ f"Error using confidence in sample view for polygon {idx}: {e}"
1740
+ )
1741
+ ax.plot(pixel_x, pixel_y, color="red", linewidth=1.5)
1742
+ else:
1743
+ ax.plot(pixel_x, pixel_y, color="red", linewidth=1.5)
809
1744
  except Exception as e:
810
1745
  print(f"Error plotting polygon in sample view: {e}")
811
1746
 
@@ -828,5 +1763,3 @@ class BuildingFootprintExtractor:
828
1763
  plt.tight_layout()
829
1764
  plt.savefig(sample_output, dpi=300, bbox_inches="tight")
830
1765
  print(f"Sample visualization saved to {sample_output}")
831
-
832
- return True