File size: 6,302 Bytes
fe44f24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ff8f69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe44f24
5ff8f69
 
fe44f24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ff8f69
 
 
 
 
 
 
 
 
 
 
 
 
fe44f24
5ff8f69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe44f24
 
 
5ff8f69
fe44f24
 
5ff8f69
fe44f24
 
 
 
 
 
 
 
 
 
5ff8f69
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import numpy as np

SEMANTIC_REJECT = {
    "background", "wall", "sky", "floor", "ground",
    "ceiling", "road", "pavement", "surface"
}

MIN_AREA_FRACTION = 0.001
MAX_AREA_FRACTION = 0.80
DUPLICATE_IOU_THRESHOLD = 0.70


def compute_iou(a, b):
    ax2 = a["x"] + a["w"]
    ay2 = a["y"] + a["h"]
    bx2 = b["x"] + b["w"]
    by2 = b["y"] + b["h"]

    ix1 = max(a["x"], b["x"])
    iy1 = max(a["y"], b["y"])
    ix2 = min(ax2, bx2)
    iy2 = min(ay2, by2)

    if ix2 <= ix1 or iy2 <= iy1:
        return 0.0

    intersection = (ix2 - ix1) * (iy2 - iy1)
    union = a["w"] * a["h"] + b["w"] * b["h"] - intersection
    return intersection / union if union > 0 else 0.0


def is_fully_inside(inner, outer):
    return (
        inner["x"] >= outer["x"] and
        inner["y"] >= outer["y"] and
        inner["x"] + inner["w"] <= outer["x"] + outer["w"] and
        inner["y"] + inner["h"] <= outer["y"] + outer["h"]
    )


def semantic_filter(proposals):
    accepted, rejected = [], []
    for p in proposals:
        label = p.get("label", "").lower().strip()
        if label in SEMANTIC_REJECT:
            rejected.append((p, "semantic"))
        else:
            accepted.append(p)
    return accepted, rejected


def area_filter(proposals, img_w, img_h):
    image_area = img_w * img_h
    accepted, rejected = [], []
    for p in proposals:
        fraction = (p["w"] * p["h"]) / image_area
        if fraction < MIN_AREA_FRACTION:
            rejected.append((p, "too small"))
        elif fraction > MAX_AREA_FRACTION:
            rejected.append((p, "too large"))
        else:
            accepted.append(p)
    return accepted, rejected


def deduplicate(proposals):
    kept, rejected = [], []
    for candidate in proposals:
        duplicate = False
        for accepted in kept:
            if compute_iou(candidate, accepted) > DUPLICATE_IOU_THRESHOLD:
                duplicate = True
                break
        if duplicate:
            rejected.append((candidate, "duplicate"))
        else:
            kept.append(candidate)
    return kept, rejected


def container_filter(proposals):
    accepted, rejected = [], []
    for i, candidate in enumerate(proposals):
        others = [p for j, p in enumerate(proposals) if j != i]
        children_inside = sum(1 for o in others if is_fully_inside(o, candidate))
        if children_inside >= 2:
            rejected.append((candidate, "container"))
        else:
            accepted.append(candidate)
    return accepted, rejected


def reject_text_inside_objects(text_proposals, accepted_objects):
    kept = []
    rejected = []

    for text in text_proposals:
        text_area = text["w"] * text["h"]

        remove = False

        for obj in accepted_objects:
            overlap = intersection_area(text, obj)

            if overlap / text_area >= 0.90:
                remove = True
                break

        if remove:
            rejected.append((text, "inside object"))
        else:
            kept.append(text)

    return kept, rejected


def compute_groups(layers):
    groups = []
    group_counter = 0
    annotated = [dict(l, group_id=None, group_role=None) for l in layers]

    for i, root in enumerate(annotated):
        children = []
        for j, other in enumerate(annotated):
            if i == j:
                continue
            if is_fully_inside(other, root):
                children.append(j)

        if not children:
            continue

        free = [j for j in children if annotated[j]["group_id"] is None]
        if not free:
            continue

        group_id    = f"group_{group_counter}"
        group_label = root.get("label") or root.get("text") or f"Group {group_counter}"
        group_counter += 1

        annotated[i]["group_id"]   = group_id
        annotated[i]["group_role"] = "root"
        for j in free:
            annotated[j]["group_id"]   = group_id
            annotated[j]["group_role"] = "child"

        groups.append({
            "id":    group_id,
            "label": group_label,
            "type":  "group",
        })

    return annotated, groups


def print_proposal_report(accepted, all_rejected):
    print("\n[PROPOSAL] ── Proposal Report ──────────────────")
    for p in accepted:
        label = p.get("label") or p.get("text", "")[:20]
        print(f"[PROPOSAL]   {label:<25} Accepted")
    for p, reason in all_rejected:
        label = p.get("label") or p.get("text", "")[:20]
        print(f"[PROPOSAL]   {label:<25} Rejected ({reason})")
    print("[PROPOSAL] ─────────────────────────────────────\n")

def reject_text_like_objects(object_proposals, text_proposals):
    kept = []
    rejected = []

    for obj in object_proposals:
        obj_area = obj["w"] * obj["h"]
        remove = False

        for text in text_proposals:
            overlap = intersection_area(obj, text)

            if overlap == 0:
                continue

            text_area = text["w"] * text["h"]

            text_inside = overlap / text_area
            object_is_text = overlap / obj_area

            if text_inside > 0.90 and object_is_text > 0.70:
                remove = True
                break

        if remove:
            rejected.append((obj, "text object"))
        else:
            kept.append(obj)

    return kept, rejected

def run_proposal_engine(object_proposals, text_proposals, img_w, img_h):
    all_rejected = []

    after_semantic, rej = semantic_filter(object_proposals)
    after_text, rej = reject_text_like_objects(after_semantic, text_proposals)
    all_rejected.extend(rej)

    after_area, rej = area_filter(after_text, img_w, img_h)
    all_rejected.extend(rej)

    after_dedup, rej = deduplicate(after_area)
    all_rejected.extend(rej)

    after_container, rej = container_filter(after_dedup)
    all_rejected.extend(rej)

    print_proposal_report(after_container, all_rejected)

    return after_container

def intersection_area(a, b):
    x1 = max(a["x"], b["x"])
    y1 = max(a["y"], b["y"])
    x2 = min(a["x"] + a["w"], b["x"] + b["w"])
    y2 = min(a["y"] + a["h"], b["y"] + b["h"])

    if x2 <= x1 or y2 <= y1:
        return 0

    return (x2 - x1) * (y2 - y1)