crop-yield-prediction / spatial_graph.py
asmitha2025
Initial Hugging Face Space deploy with LFS
cb017f0
Raw
History Blame Contribute Delete
4.58 kB
import numpy as np
from dataclasses import dataclass, field
from typing import Dict, Tuple, Optional, List
@dataclass
class ObjectState:
obj_id: int
class_name: str
bbox: Tuple[int, int, int, int] # x1, y1, x2, y2
center: Tuple[float, float]
area: float
embedding: Optional[np.ndarray] = None # None on delta frames
@dataclass
class Relation:
distance: float # Euclidean pixel distance between centers
angle: float # radians, obj_i β†’ obj_j
size_ratio: float # area_j / area_i
class SpatialGraph:
"""
Directed spatial graph of detected objects in one frame.
Nodes = objects, Edges = pairwise spatial relations.
"""
def __init__(self):
self.objects: Dict[int, ObjectState] = {}
self.relations: Dict[Tuple[int, int], Relation] = {}
# ------------------------------------------------------------------
# Build
# ------------------------------------------------------------------
def add_object(self, obj: ObjectState) -> None:
self.objects[obj.obj_id] = obj
def build_relations(self, width: float = 640.0, height: float = 480.0) -> None:
ids = sorted(self.objects.keys())
self.relations = {}
if len(ids) == 1:
# Single object case: relate to the frame center (normalized against screen diagonal)
oid = ids[0]
o = self.objects[oid]
cx, cy = width / 2.0, height / 2.0
dx = o.center[0] - cx
dy = o.center[1] - cy
max_dist = np.hypot(cx, cy)
self.relations[(oid, oid)] = Relation(
distance=float(np.hypot(dx, dy) / (max_dist + 1e-6)),
angle=float(np.arctan2(dy, dx)),
size_ratio=float(o.area / (width * height + 1e-6)),
)
else:
for i in range(len(ids)):
for j in range(i + 1, len(ids)):
id1, id2 = ids[i], ids[j]
o1, o2 = self.objects[id1], self.objects[id2]
dx = o2.center[0] - o1.center[0]
dy = o2.center[1] - o1.center[1]
self.relations[(id1, id2)] = Relation(
distance=float(np.hypot(dx, dy)),
angle=float(np.arctan2(dy, dx)),
size_ratio=float(o2.area / (o1.area + 1e-6)),
)
# ------------------------------------------------------------------
# Delta
# ------------------------------------------------------------------
def compute_delta(self, other: "SpatialGraph") -> dict:
"""
Compute structural difference between self (anchor) and other (current).
Returns
-------
dict with keys:
total_magnitude – mean normalized distance change (0 = identical)
relation_deltas – per-pair change breakdown
new_objects – IDs that appeared in `other` but not in `self`
lost_objects – IDs in `self` but missing in `other`
"""
delta: dict = {
"total_magnitude": 0.0,
"relation_deltas": {},
"new_objects": [],
"lost_objects": [],
}
common_pairs: set = set(self.relations) & set(other.relations)
for pair in common_pairs:
r_anchor = self.relations[pair]
r_current = other.relations[pair]
d_dist = abs(r_current.distance - r_anchor.distance)
d_angle = abs(r_current.angle - r_anchor.angle)
d_size = abs(r_current.size_ratio - r_anchor.size_ratio)
# Normalize distance change relative to anchor distance
if pair[0] == pair[1]:
# Single object: d_dist is already normalized to screen diagonal
norm_dist = d_dist
else:
norm_dist = d_dist / (r_anchor.distance + 1e-6)
delta["relation_deltas"][pair] = {
"delta_distance": d_dist,
"delta_angle": d_angle,
"delta_size_ratio": d_size,
"magnitude": norm_dist,
}
delta["total_magnitude"] += norm_dist
if common_pairs:
delta["total_magnitude"] /= len(common_pairs)
delta["new_objects"] = [oid for oid in other.objects if oid not in self.objects]
delta["lost_objects"] = [oid for oid in self.objects if oid not in other.objects]
return delta