Spaces:
Sleeping
Sleeping
Commit ·
94cbea0
1
Parent(s): ce1e651
Add 3D building analysis: shadow-based height/stories estimation and construction stage classification
Browse files- app/detection_engine.py +275 -12
- app/main.py +3 -0
- static/js/app.js +6 -0
- templates/index.html +4 -1
app/detection_engine.py
CHANGED
|
@@ -439,14 +439,35 @@ def visualize_changes(img1, img2, change_mask, regions=None):
|
|
| 439 |
for c in range(3):
|
| 440 |
overlay[:, :, c] = overlay[:, :, c] * (1 - mask_float * alpha) + red_layer[:, :, c] * mask_float * alpha
|
| 441 |
|
| 442 |
-
# Draw
|
| 443 |
if regions:
|
| 444 |
-
|
| 445 |
for r in regions:
|
| 446 |
x, y, w, h = r["bbox"]
|
| 447 |
-
cv2.rectangle(
|
| 448 |
-
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
|
| 451 |
return np.clip(overlay, 0, 255).astype(np.uint8)
|
| 452 |
|
|
@@ -760,13 +781,239 @@ def classify_with_ensemble(image_region, bbox, num_sub=4):
|
|
| 760 |
|
| 761 |
|
| 762 |
# ---------------------------------------------------------------------------
|
| 763 |
-
# 11.
|
| 764 |
# ---------------------------------------------------------------------------
|
| 765 |
|
| 766 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
"""
|
| 768 |
Find connected change regions, classify as ground-level changes only.
|
| 769 |
Transient objects (people, cars, animals) are filtered out.
|
|
|
|
| 770 |
"""
|
| 771 |
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(change_mask, connectivity=8)
|
| 772 |
change_regions = []
|
|
@@ -788,19 +1035,34 @@ def analyze_change_regions(change_mask, image, min_area=200, use_ensemble=True):
|
|
| 788 |
else:
|
| 789 |
object_type, confidence = classify_object_type(image, (x, y, w, h))
|
| 790 |
|
| 791 |
-
# None means transient / irrelevant → skip
|
| 792 |
if object_type is None:
|
| 793 |
continue
|
| 794 |
|
| 795 |
region_id += 1
|
| 796 |
-
|
| 797 |
"id": region_id,
|
| 798 |
"area": area,
|
| 799 |
"bbox": (x, y, w, h),
|
| 800 |
"center": (int(cx), int(cy)),
|
| 801 |
"object_type": object_type,
|
| 802 |
"confidence": confidence,
|
| 803 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
|
| 805 |
change_regions.sort(key=lambda r: r["area"], reverse=True)
|
| 806 |
return change_regions
|
|
@@ -830,9 +1092,10 @@ def run_detection(before_pil, after_pil, method="AI-Based Deep Learning",
|
|
| 830 |
else:
|
| 831 |
change_mask = hybrid_method(before_array, after_array)
|
| 832 |
|
| 833 |
-
change_regions = analyze_change_regions(
|
|
|
|
|
|
|
| 834 |
|
| 835 |
-
# Color-coded visualization using region classifications
|
| 836 |
result_image = visualize_changes(before_array, after_array, change_mask, regions=change_regions)
|
| 837 |
|
| 838 |
total_pixels = int(change_mask.shape[0] * change_mask.shape[1])
|
|
|
|
| 439 |
for c in range(3):
|
| 440 |
overlay[:, :, c] = overlay[:, :, c] * (1 - mask_float * alpha) + red_layer[:, :, c] * mask_float * alpha
|
| 441 |
|
| 442 |
+
# Draw outlines and labels for each region
|
| 443 |
if regions:
|
| 444 |
+
overlay_uint8 = np.clip(overlay, 0, 255).astype(np.uint8)
|
| 445 |
for r in regions:
|
| 446 |
x, y, w, h = r["bbox"]
|
| 447 |
+
cv2.rectangle(overlay_uint8, (x, y), (x + w, y + h), (255, 255, 255), 1)
|
| 448 |
+
|
| 449 |
+
# Annotate building regions with 3D info
|
| 450 |
+
stories = r.get("estimated_stories")
|
| 451 |
+
stage = r.get("construction_stage")
|
| 452 |
+
if stories is not None or stage is not None:
|
| 453 |
+
parts = []
|
| 454 |
+
if stories is not None:
|
| 455 |
+
parts.append(f"{stories}F")
|
| 456 |
+
if stage and stage != "Unknown":
|
| 457 |
+
parts.append(stage)
|
| 458 |
+
label = " | ".join(parts)
|
| 459 |
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
| 460 |
+
font_scale = max(0.35, min(0.55, w / 200))
|
| 461 |
+
thickness = 1
|
| 462 |
+
(tw, th), _ = cv2.getTextSize(label, font, font_scale, thickness)
|
| 463 |
+
lx = x
|
| 464 |
+
ly = max(th + 4, y - 6)
|
| 465 |
+
# Background rectangle for readability
|
| 466 |
+
cv2.rectangle(overlay_uint8, (lx, ly - th - 4), (lx + tw + 6, ly + 2),
|
| 467 |
+
(0, 0, 0), cv2.FILLED)
|
| 468 |
+
cv2.putText(overlay_uint8, label, (lx + 3, ly - 2), font,
|
| 469 |
+
font_scale, (255, 255, 255), thickness, cv2.LINE_AA)
|
| 470 |
+
return overlay_uint8
|
| 471 |
|
| 472 |
return np.clip(overlay, 0, 255).astype(np.uint8)
|
| 473 |
|
|
|
|
| 781 |
|
| 782 |
|
| 783 |
# ---------------------------------------------------------------------------
|
| 784 |
+
# 11. 3D Building Analysis — height estimation + construction stage
|
| 785 |
# ---------------------------------------------------------------------------
|
| 786 |
|
| 787 |
+
_BUILDING_TYPES = {"New Construction/Building", "Demolition/Clearing"}
|
| 788 |
+
_STORY_HEIGHT_M = 3.0 # assumed metres per story
|
| 789 |
+
|
| 790 |
+
|
| 791 |
+
def _detect_shadow_region(before_gray, after_gray, bbox, expand=0.6):
|
| 792 |
+
"""
|
| 793 |
+
Find new shadow pixels adjacent to a building bbox.
|
| 794 |
+
Returns a binary mask of likely shadow pixels in the expanded bbox area.
|
| 795 |
+
"""
|
| 796 |
+
x, y, w, h = bbox
|
| 797 |
+
img_h, img_w = after_gray.shape[:2]
|
| 798 |
+
|
| 799 |
+
# Expand bbox to capture shadows cast beside the building
|
| 800 |
+
ex = int(w * expand)
|
| 801 |
+
ey = int(h * expand)
|
| 802 |
+
x1 = max(0, x - ex)
|
| 803 |
+
y1 = max(0, y - ey)
|
| 804 |
+
x2 = min(img_w, x + w + ex)
|
| 805 |
+
y2 = min(img_h, y + h + ey)
|
| 806 |
+
|
| 807 |
+
before_crop = before_gray[y1:y2, x1:x2].astype(np.float32)
|
| 808 |
+
after_crop = after_gray[y1:y2, x1:x2].astype(np.float32)
|
| 809 |
+
|
| 810 |
+
if before_crop.size == 0 or after_crop.size == 0:
|
| 811 |
+
return None, 0
|
| 812 |
+
|
| 813 |
+
# New shadow = pixels that got significantly darker in the after image
|
| 814 |
+
darkening = before_crop - after_crop
|
| 815 |
+
dark_thresh = max(25, np.std(darkening) * 1.5)
|
| 816 |
+
shadow_mask = (darkening > dark_thresh).astype(np.uint8) * 255
|
| 817 |
+
|
| 818 |
+
# Remove shadow pixels inside the building footprint itself
|
| 819 |
+
bx1, by1 = x - x1, y - y1
|
| 820 |
+
bx2, by2 = bx1 + w, by1 + h
|
| 821 |
+
bx1, by1 = max(0, bx1), max(0, by1)
|
| 822 |
+
bx2 = min(shadow_mask.shape[1], bx2)
|
| 823 |
+
by2 = min(shadow_mask.shape[0], by2)
|
| 824 |
+
shadow_mask[by1:by2, bx1:bx2] = 0
|
| 825 |
+
|
| 826 |
+
# Clean noise
|
| 827 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
|
| 828 |
+
shadow_mask = cv2.morphologyEx(shadow_mask, cv2.MORPH_OPEN, kernel)
|
| 829 |
+
|
| 830 |
+
shadow_pixels = np.sum(shadow_mask > 0)
|
| 831 |
+
return shadow_mask, shadow_pixels
|
| 832 |
+
|
| 833 |
+
|
| 834 |
+
def estimate_building_height(before_img, after_img, bbox, features):
|
| 835 |
+
"""
|
| 836 |
+
Estimate building stories and height from shadow length and footprint geometry.
|
| 837 |
+
Returns (estimated_stories, estimated_height_m).
|
| 838 |
+
"""
|
| 839 |
+
before_gray = cv2.cvtColor(before_img, cv2.COLOR_RGB2GRAY)
|
| 840 |
+
after_gray = cv2.cvtColor(after_img, cv2.COLOR_RGB2GRAY)
|
| 841 |
+
x, y, w, h = bbox
|
| 842 |
+
|
| 843 |
+
shadow_mask, shadow_px = _detect_shadow_region(before_gray, after_gray, bbox)
|
| 844 |
+
|
| 845 |
+
short_side = max(min(w, h), 1)
|
| 846 |
+
footprint_area = w * h
|
| 847 |
+
|
| 848 |
+
# --- Shadow-based estimate ---
|
| 849 |
+
shadow_ratio = 0.0
|
| 850 |
+
if shadow_mask is not None and shadow_px > 20:
|
| 851 |
+
# Measure max extent of shadow perpendicular to building edge
|
| 852 |
+
coords = np.column_stack(np.where(shadow_mask > 0))
|
| 853 |
+
if len(coords) > 5:
|
| 854 |
+
# Shadow length = extent along the longer axis of shadow cluster
|
| 855 |
+
spread_y = coords[:, 0].max() - coords[:, 0].min()
|
| 856 |
+
spread_x = coords[:, 1].max() - coords[:, 1].min()
|
| 857 |
+
shadow_length = max(spread_y, spread_x)
|
| 858 |
+
shadow_ratio = shadow_length / short_side
|
| 859 |
+
|
| 860 |
+
# --- Footprint-based estimate ---
|
| 861 |
+
aspect = max(w, h) / max(short_side, 1)
|
| 862 |
+
# Compact footprints (aspect < 2.5) tend to be multi-story; elongated are single-story
|
| 863 |
+
footprint_factor = 1.0
|
| 864 |
+
if aspect > 3.0:
|
| 865 |
+
footprint_factor = 0.5 # likely single-story warehouse/industrial
|
| 866 |
+
elif aspect < 1.5 and footprint_area > 2000:
|
| 867 |
+
footprint_factor = 1.3 # compact large footprint = likely taller
|
| 868 |
+
|
| 869 |
+
# --- Texture regularity bonus ---
|
| 870 |
+
# Buildings with low orientation entropy (regular structure) tend to be taller
|
| 871 |
+
regularity_bonus = 0.0
|
| 872 |
+
if features and features.get("orientation_entropy", 3.0) < 2.2:
|
| 873 |
+
regularity_bonus = 0.5
|
| 874 |
+
|
| 875 |
+
# --- Combine signals ---
|
| 876 |
+
# Base: shadow ratio maps ~0.3-0.5 per story in typical nadir imagery
|
| 877 |
+
if shadow_ratio > 0.1:
|
| 878 |
+
raw_stories = shadow_ratio / 0.35
|
| 879 |
+
else:
|
| 880 |
+
# No clear shadow: use footprint area as rough proxy
|
| 881 |
+
if footprint_area > 5000:
|
| 882 |
+
raw_stories = 3.0
|
| 883 |
+
elif footprint_area > 2000:
|
| 884 |
+
raw_stories = 2.0
|
| 885 |
+
else:
|
| 886 |
+
raw_stories = 1.0
|
| 887 |
+
|
| 888 |
+
raw_stories = raw_stories * footprint_factor + regularity_bonus
|
| 889 |
+
stories = max(1, min(50, int(round(raw_stories))))
|
| 890 |
+
height_m = round(stories * _STORY_HEIGHT_M, 1)
|
| 891 |
+
|
| 892 |
+
return stories, height_m
|
| 893 |
+
|
| 894 |
+
|
| 895 |
+
def classify_construction_stage(features, bbox):
|
| 896 |
+
"""
|
| 897 |
+
Classify construction stage from visual features.
|
| 898 |
+
Returns (stage_name, confidence).
|
| 899 |
+
"""
|
| 900 |
+
if features is None:
|
| 901 |
+
return "Unknown", 0.0
|
| 902 |
+
|
| 903 |
+
w, h = bbox[2], bbox[3]
|
| 904 |
+
area = w * h
|
| 905 |
+
|
| 906 |
+
scores = {
|
| 907 |
+
"Foundation": 0.0,
|
| 908 |
+
"Structural": 0.0,
|
| 909 |
+
"Under Construction": 0.0,
|
| 910 |
+
"Complete": 0.0,
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
tex = features.get("texture_std", 30)
|
| 914 |
+
edge = features.get("edge_density", 40)
|
| 915 |
+
orient = features.get("orientation_entropy", 2.5)
|
| 916 |
+
homog = features.get("color_homogeneity", 25)
|
| 917 |
+
bright = features.get("brightness", 60)
|
| 918 |
+
sat = features.get("saturation", 50)
|
| 919 |
+
glcm = features.get("glcm_contrast", 500)
|
| 920 |
+
lbp_var = features.get("lbp_variance", 0.04)
|
| 921 |
+
|
| 922 |
+
# --- Foundation ---
|
| 923 |
+
# Flat, low-texture, soil/concrete colored, homogeneous
|
| 924 |
+
if tex < 22:
|
| 925 |
+
scores["Foundation"] += 0.25
|
| 926 |
+
if edge < 30:
|
| 927 |
+
scores["Foundation"] += 0.20
|
| 928 |
+
if homog < 20:
|
| 929 |
+
scores["Foundation"] += 0.20
|
| 930 |
+
if 40 <= bright <= 75:
|
| 931 |
+
scores["Foundation"] += 0.15
|
| 932 |
+
if sat < 60:
|
| 933 |
+
scores["Foundation"] += 0.10
|
| 934 |
+
if lbp_var < 0.03:
|
| 935 |
+
scores["Foundation"] += 0.10
|
| 936 |
+
|
| 937 |
+
# --- Structural/Framing ---
|
| 938 |
+
# High edges, geometric regularity, high contrast grid patterns
|
| 939 |
+
if edge > 50:
|
| 940 |
+
scores["Structural"] += 0.25
|
| 941 |
+
if orient < 2.2:
|
| 942 |
+
scores["Structural"] += 0.20
|
| 943 |
+
if glcm > 800:
|
| 944 |
+
scores["Structural"] += 0.20
|
| 945 |
+
if tex > 30:
|
| 946 |
+
scores["Structural"] += 0.15
|
| 947 |
+
if homog > 30:
|
| 948 |
+
scores["Structural"] += 0.10
|
| 949 |
+
if area > 1000:
|
| 950 |
+
scores["Structural"] += 0.10
|
| 951 |
+
|
| 952 |
+
# --- Under Construction ---
|
| 953 |
+
# Mixed materials, irregular texture, medium-high edge density
|
| 954 |
+
if 25 < tex < 50:
|
| 955 |
+
scores["Under Construction"] += 0.20
|
| 956 |
+
if 35 < edge < 65:
|
| 957 |
+
scores["Under Construction"] += 0.20
|
| 958 |
+
if orient > 2.6:
|
| 959 |
+
scores["Under Construction"] += 0.20
|
| 960 |
+
if homog > 25:
|
| 961 |
+
scores["Under Construction"] += 0.15
|
| 962 |
+
if 0.03 < lbp_var < 0.07:
|
| 963 |
+
scores["Under Construction"] += 0.15
|
| 964 |
+
if sat < 80:
|
| 965 |
+
scores["Under Construction"] += 0.10
|
| 966 |
+
|
| 967 |
+
# --- Complete ---
|
| 968 |
+
# Uniform roof, clean edges, low entropy, consistent color
|
| 969 |
+
if tex < 28:
|
| 970 |
+
scores["Complete"] += 0.20
|
| 971 |
+
if orient < 2.3:
|
| 972 |
+
scores["Complete"] += 0.25
|
| 973 |
+
if homog < 22:
|
| 974 |
+
scores["Complete"] += 0.20
|
| 975 |
+
if edge > 25:
|
| 976 |
+
scores["Complete"] += 0.10
|
| 977 |
+
if lbp_var < 0.04:
|
| 978 |
+
scores["Complete"] += 0.15
|
| 979 |
+
if bright > 50:
|
| 980 |
+
scores["Complete"] += 0.10
|
| 981 |
+
|
| 982 |
+
best = max(scores, key=scores.get)
|
| 983 |
+
conf = scores[best]
|
| 984 |
+
|
| 985 |
+
if conf < 0.25:
|
| 986 |
+
return "Unknown", conf
|
| 987 |
+
return best, min(conf, 1.0)
|
| 988 |
+
|
| 989 |
+
|
| 990 |
+
def analyze_building_3d(before_img, after_img, region, features):
|
| 991 |
+
"""
|
| 992 |
+
Run 3D analysis on a single building/construction region.
|
| 993 |
+
Enriches the region dict with stories, height, and construction stage.
|
| 994 |
+
"""
|
| 995 |
+
bbox = region["bbox"]
|
| 996 |
+
|
| 997 |
+
stories, height_m = estimate_building_height(before_img, after_img, bbox, features)
|
| 998 |
+
stage, stage_conf = classify_construction_stage(features, bbox)
|
| 999 |
+
|
| 1000 |
+
region["estimated_stories"] = stories
|
| 1001 |
+
region["estimated_height_m"] = height_m
|
| 1002 |
+
region["construction_stage"] = stage
|
| 1003 |
+
region["construction_stage_confidence"] = stage_conf
|
| 1004 |
+
return region
|
| 1005 |
+
|
| 1006 |
+
|
| 1007 |
+
# ---------------------------------------------------------------------------
|
| 1008 |
+
# 12. Region analysis
|
| 1009 |
+
# ---------------------------------------------------------------------------
|
| 1010 |
+
|
| 1011 |
+
def analyze_change_regions(change_mask, image, min_area=200, use_ensemble=True,
|
| 1012 |
+
before_img=None):
|
| 1013 |
"""
|
| 1014 |
Find connected change regions, classify as ground-level changes only.
|
| 1015 |
Transient objects (people, cars, animals) are filtered out.
|
| 1016 |
+
Building regions get enriched with 3D analysis (stories, height, stage).
|
| 1017 |
"""
|
| 1018 |
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(change_mask, connectivity=8)
|
| 1019 |
change_regions = []
|
|
|
|
| 1035 |
else:
|
| 1036 |
object_type, confidence = classify_object_type(image, (x, y, w, h))
|
| 1037 |
|
|
|
|
| 1038 |
if object_type is None:
|
| 1039 |
continue
|
| 1040 |
|
| 1041 |
region_id += 1
|
| 1042 |
+
region = {
|
| 1043 |
"id": region_id,
|
| 1044 |
"area": area,
|
| 1045 |
"bbox": (x, y, w, h),
|
| 1046 |
"center": (int(cx), int(cy)),
|
| 1047 |
"object_type": object_type,
|
| 1048 |
"confidence": confidence,
|
| 1049 |
+
"estimated_stories": None,
|
| 1050 |
+
"estimated_height_m": None,
|
| 1051 |
+
"construction_stage": None,
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
# 3D analysis for building/construction regions
|
| 1055 |
+
if object_type in _BUILDING_TYPES and before_img is not None:
|
| 1056 |
+
pad = 5
|
| 1057 |
+
ry1 = max(0, y - pad)
|
| 1058 |
+
ry2 = min(image.shape[0], y + h + pad)
|
| 1059 |
+
rx1 = max(0, x - pad)
|
| 1060 |
+
rx2 = min(image.shape[1], x + w + pad)
|
| 1061 |
+
crop = image[ry1:ry2, rx1:rx2]
|
| 1062 |
+
feats = extract_advanced_features(crop) if crop.size > 0 else None
|
| 1063 |
+
analyze_building_3d(before_img, image, region, feats)
|
| 1064 |
+
|
| 1065 |
+
change_regions.append(region)
|
| 1066 |
|
| 1067 |
change_regions.sort(key=lambda r: r["area"], reverse=True)
|
| 1068 |
return change_regions
|
|
|
|
| 1092 |
else:
|
| 1093 |
change_mask = hybrid_method(before_array, after_array)
|
| 1094 |
|
| 1095 |
+
change_regions = analyze_change_regions(
|
| 1096 |
+
change_mask, after_array, min_area=200, before_img=before_array
|
| 1097 |
+
)
|
| 1098 |
|
|
|
|
| 1099 |
result_image = visualize_changes(before_array, after_array, change_mask, regions=change_regions)
|
| 1100 |
|
| 1101 |
total_pixels = int(change_mask.shape[0] * change_mask.shape[1])
|
app/main.py
CHANGED
|
@@ -205,6 +205,9 @@ async def detect(
|
|
| 205 |
"bbox": {"x": int(r["bbox"][0]), "y": int(r["bbox"][1]), "w": int(r["bbox"][2]), "h": int(r["bbox"][3])},
|
| 206 |
"objectType": str(r["object_type"]),
|
| 207 |
"confidence": float(r["confidence"]),
|
|
|
|
|
|
|
|
|
|
| 208 |
}
|
| 209 |
for r in change_regions
|
| 210 |
]
|
|
|
|
| 205 |
"bbox": {"x": int(r["bbox"][0]), "y": int(r["bbox"][1]), "w": int(r["bbox"][2]), "h": int(r["bbox"][3])},
|
| 206 |
"objectType": str(r["object_type"]),
|
| 207 |
"confidence": float(r["confidence"]),
|
| 208 |
+
"estimatedStories": r.get("estimated_stories"),
|
| 209 |
+
"estimatedHeightM": float(r["estimated_height_m"]) if r.get("estimated_height_m") is not None else None,
|
| 210 |
+
"constructionStage": r.get("construction_stage"),
|
| 211 |
}
|
| 212 |
for r in change_regions
|
| 213 |
]
|
static/js/app.js
CHANGED
|
@@ -262,11 +262,17 @@ function showResult(data) {
|
|
| 262 |
tbody.innerHTML = '';
|
| 263 |
(data.regions || []).slice(0, 50).forEach((r) => {
|
| 264 |
const tr = document.createElement('tr');
|
|
|
|
|
|
|
|
|
|
| 265 |
tr.innerHTML = `
|
| 266 |
<td>${r.id}</td>
|
| 267 |
<td>${r.objectType}</td>
|
| 268 |
<td>${(r.confidence * 100).toFixed(1)}%</td>
|
| 269 |
<td>${r.area.toLocaleString()}</td>
|
|
|
|
|
|
|
|
|
|
| 270 |
<td>(${r.center.x}, ${r.center.y})</td>
|
| 271 |
`;
|
| 272 |
tbody.appendChild(tr);
|
|
|
|
| 262 |
tbody.innerHTML = '';
|
| 263 |
(data.regions || []).slice(0, 50).forEach((r) => {
|
| 264 |
const tr = document.createElement('tr');
|
| 265 |
+
const stories = r.estimatedStories != null ? r.estimatedStories : '—';
|
| 266 |
+
const height = r.estimatedHeightM != null ? r.estimatedHeightM + ' m' : '—';
|
| 267 |
+
const stage = r.constructionStage && r.constructionStage !== 'Unknown' ? r.constructionStage : '—';
|
| 268 |
tr.innerHTML = `
|
| 269 |
<td>${r.id}</td>
|
| 270 |
<td>${r.objectType}</td>
|
| 271 |
<td>${(r.confidence * 100).toFixed(1)}%</td>
|
| 272 |
<td>${r.area.toLocaleString()}</td>
|
| 273 |
+
<td>${stories}</td>
|
| 274 |
+
<td>${height}</td>
|
| 275 |
+
<td>${stage}</td>
|
| 276 |
<td>(${r.center.x}, ${r.center.y})</td>
|
| 277 |
`;
|
| 278 |
tbody.appendChild(tr);
|
templates/index.html
CHANGED
|
@@ -253,6 +253,9 @@
|
|
| 253 |
<th>Ground Change Type</th>
|
| 254 |
<th>Confidence</th>
|
| 255 |
<th>Area (px)</th>
|
|
|
|
|
|
|
|
|
|
| 256 |
<th>Center</th>
|
| 257 |
</tr>
|
| 258 |
</thead>
|
|
@@ -288,6 +291,6 @@
|
|
| 288 |
</div>
|
| 289 |
</div>
|
| 290 |
|
| 291 |
-
<script src="/static/js/app.js?v=
|
| 292 |
</body>
|
| 293 |
</html>
|
|
|
|
| 253 |
<th>Ground Change Type</th>
|
| 254 |
<th>Confidence</th>
|
| 255 |
<th>Area (px)</th>
|
| 256 |
+
<th>Stories</th>
|
| 257 |
+
<th>Height</th>
|
| 258 |
+
<th>Stage</th>
|
| 259 |
<th>Center</th>
|
| 260 |
</tr>
|
| 261 |
</thead>
|
|
|
|
| 291 |
</div>
|
| 292 |
</div>
|
| 293 |
|
| 294 |
+
<script src="/static/js/app.js?v=11"></script>
|
| 295 |
</body>
|
| 296 |
</html>
|