Kalp Kanungo commited on
Commit
c858478
·
0 Parent(s):

Initial commit - Multimodal AI project

Browse files
.gitignore ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+
7
+ # Environment
8
+ .env
9
+ .venv
10
+ env/
11
+ venv/
12
+
13
+ # Conda
14
+ conda-meta/
15
+
16
+ # Mac
17
+ .DS_Store
18
+
19
+ # Data
20
+ data/
21
+ !data/.gitkeep
22
+
23
+ # Models
24
+ models/
25
+ *.pt
26
+ *.pth
27
+ *.bin
28
+
29
+ # Logs
30
+ *.log
31
+
32
+ # Jupyter
33
+ .ipynb_checkpoints
34
+
35
+ # HuggingFace cache
36
+ .cache/
37
+ huggingface/
38
+
39
+
40
+ gradio_cached_examples/
41
+
42
+
43
+ *.zip
44
+ *.tar
README.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Scene Graph Generator
3
+ emoji: 🧠
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: false
9
+ ---
10
+
11
+ # 🧠 Scene Graph Generator (Multimodal AI System)
12
+
13
+ A multimodal computer vision system that takes an input image, detects objects, predicts relationships between them, constructs a structured scene graph, and generates a natural language description of the scene.
14
+
15
+ 🔗 **Live Demo (Hugging Face Spaces):**
16
+ https://huggingface.co/spaces/<your-username>/scene-graph-generator
17
+
18
+ ---
19
+
20
+ # 🚀 Features
21
+
22
+ - 🖼️ Object Detection using DETR (ResNet-50)
23
+ - 🔗 Relationship Prediction (Custom Trained Model)
24
+ - 📐 Spatial Reasoning (Hybrid AI with Geometry Rules)
25
+ - 🧩 Scene Graph Construction (Directed Graph)
26
+ - 📊 Graph Visualization (NetworkX + Matplotlib)
27
+ - 🧠 Graph-to-Text Generation (FLAN-T5)
28
+ - 🌐 Interactive UI (Gradio)
29
+ - ☁️ Deployed on Hugging Face Spaces (CPU)
30
+
31
+ ---
32
+
33
+ # 🧠 How It Works (End-to-End Pipeline)
34
+
35
+ ### 1. Input
36
+ - User uploads an image (JPG/PNG) via Gradio UI
37
+ - Image is converted from PIL → OpenCV format
38
+
39
+ ---
40
+
41
+ ### 2. Object Detection
42
+ - Uses `facebook/detr-resnet-50` from Hugging Face
43
+ - Outputs:
44
+ - Object labels (COCO classes)
45
+ - Bounding boxes
46
+ - Confidence scores
47
+ - Applies threshold (≥ 0.7) to filter noise
48
+
49
+ ---
50
+
51
+ ### 3. Pairwise Object Processing
52
+ - Generates object pairs using `itertools.combinations`
53
+ - Extracts bounding boxes for each pair
54
+ - Creates union region for relation inference
55
+ - Filters duplicate object pairs
56
+
57
+ ---
58
+
59
+ ### 4. Relationship Prediction
60
+ - Custom-trained classifier on Visual Genome subset (~10K samples)
61
+ - Predicts semantic relations:
62
+ - `on`, `holding`, `behind`, etc.
63
+ - Trained using PyTorch (10 epochs)
64
+
65
+ ---
66
+
67
+ ### 5. Spatial Reasoning (Hybrid AI)
68
+ - Uses bounding box geometry to compute:
69
+ - `left_of`, `right_of`, `above`, `below`, `near`
70
+ - Hybrid logic:
71
+ - Semantic relations from model (if confident)
72
+ - Otherwise fallback to spatial rules
73
+ - Reduces bias (e.g., “everything = on”)
74
+
75
+ ---
76
+
77
+ ### 6. Graph Construction
78
+ - Builds a **directed graph (NetworkX DiGraph)**
79
+ - Nodes → objects
80
+ - Edges → relationships
81
+ - Removes duplicates and limits edges for clarity
82
+
83
+ ---
84
+
85
+ ### 7. Graph Visualization
86
+ - Uses NetworkX + Matplotlib
87
+ - Displays:
88
+ - Directed edges with labels
89
+ - Clean layout for readability
90
+
91
+ ---
92
+
93
+ ### 8. Graph → Text (NLP)
94
+ - Uses `google/flan-t5-small`
95
+ - Converts structured triples into natural language
96
+
97
+ Example:
98
+ laptop → on → table
99
+ mouse → next_to → laptop
100
+
101
+ Output:
102
+ "A laptop is placed on a table with a mouse next to it."
103
+
104
+ ---
105
+
106
+ ### 9. UI (Gradio)
107
+ - Upload image
108
+ - View:
109
+ - Scene graph
110
+ - Generated description
111
+ - Fully interactive and browser-based
112
+
113
+ ---
114
+
115
+ # 🏗️ Tech Stack
116
+
117
+ - **Computer Vision:** DETR (Hugging Face Transformers)
118
+ - **Deep Learning:** PyTorch
119
+ - **Graph Processing:** NetworkX
120
+ - **NLP:** FLAN-T5
121
+ - **Image Processing:** OpenCV
122
+ - **Frontend/UI:** Gradio
123
+ - **Deployment:** Hugging Face Spaces
124
+
125
+ ---
126
+
127
+ # 📁 Project Structure
128
+ scene-graph-generator/
129
+
130
+ ├── app.py
131
+ ├── requirements.txt
132
+ ├── README.md
133
+
134
+ ├── src/
135
+ │ ├── pipeline.py
136
+ │ ├── detection.py
137
+ │ ├── spatial_rules.py
138
+ │ ├── relationship_infer.py
139
+ │ ├── scene_graph.py
140
+ │ ├── visualization.py
141
+ │ ├── text_generation.py
142
+
143
+ ---
144
+
145
+ # ⚙️ Installation (Local Setup)
146
+
147
+ ```bash
148
+ git clone https://github.com/<your-username>/scene-graph-generator.git
149
+ cd scene-graph-generator
150
+
151
+ pip install -r requirements.txt
152
+ python app.py
app.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import cv2
3
+ import numpy as np
4
+
5
+ from src.pipeline import run_pipeline
6
+ from src.scene_graph import build_graph
7
+ from src.visualization import visualize_graph
8
+ from src.text_generation import graph_to_text
9
+
10
+
11
+ def process_image(image):
12
+ try:
13
+
14
+ image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
15
+
16
+
17
+ relations = run_pipeline(image_cv)
18
+
19
+ if not relations or len(relations) == 0:
20
+ return image, None, "No relationships detected."
21
+
22
+
23
+ G = build_graph(relations)
24
+
25
+
26
+ fig = visualize_graph(G)
27
+
28
+
29
+ caption = graph_to_text(relations)
30
+
31
+ return image, fig, caption
32
+
33
+ except Exception as e:
34
+ print("Error:", e)
35
+ return image, None, "Error processing image."
36
+
37
+
38
+ demo = gr.Interface(
39
+ fn=process_image,
40
+ inputs=gr.Image(type="pil"),
41
+ outputs=[
42
+ gr.Image(label="Input Image"),
43
+ gr.Plot(label="Scene Graph"),
44
+ gr.Textbox(label="Generated Description")
45
+ ],
46
+ title="Scene Graph Generator",
47
+ description="Upload an image → Detect objects → Predict relationships → Generate scene graph + description",
48
+ theme="soft"
49
+ )
50
+
51
+
52
+ if __name__ == "__main__":
53
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ torchvision
3
+ transformers
4
+ datasets
5
+ opencv-python
6
+ networkx
7
+ plotly
8
+ gradio
9
+ numpy
10
+ pillow
11
+ tqdm
12
+ scikit-learn
13
+ kaggle
14
+ timm
15
+ transformers
16
+ sentencepiece
src/__init__.py ADDED
File without changes
src/build_relationship_dataset.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import cv2
4
+ from tqdm import tqdm
5
+
6
+ SUBSET_PATH = "data/relationship_dataset/subset.json"
7
+ IMAGE_MAP_PATH = "data/relationship_dataset/image_paths.json"
8
+
9
+ OUTPUT_DIR = "data/relationship_dataset/images"
10
+ LABELS_PATH = "data/relationship_dataset/labels.json"
11
+
12
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
13
+
14
+ with open(SUBSET_PATH) as f:
15
+ subset = json.load(f)
16
+
17
+ with open(IMAGE_MAP_PATH) as f:
18
+ image_map = json.load(f)
19
+
20
+ labels = []
21
+
22
+ idx = 0
23
+
24
+ for item in tqdm(subset):
25
+ image_id = str(item["image_id"])
26
+
27
+ if image_id not in image_map:
28
+ continue
29
+
30
+ img_path = image_map[image_id]
31
+ img = cv2.imread(img_path)
32
+
33
+ if img is None:
34
+ continue
35
+
36
+ h_img, w_img, _ = img.shape
37
+
38
+ s = item["subject"]
39
+ o = item["object"]
40
+
41
+ x1 = min(s["x"], o["x"])
42
+ y1 = min(s["y"], o["y"])
43
+ x2 = max(s["x"] + s["w"], o["x"] + o["w"])
44
+ y2 = max(s["y"] + s["h"], o["y"] + o["h"])
45
+
46
+ x1 = max(0, x1)
47
+ y1 = max(0, y1)
48
+ x2 = min(w_img, x2)
49
+ y2 = min(h_img, y2)
50
+
51
+ crop = img[y1:y2, x1:x2]
52
+
53
+ if crop.size == 0:
54
+ continue
55
+
56
+ crop = cv2.resize(crop, (128, 128))
57
+
58
+ filename = f"{idx}.jpg"
59
+ save_path = os.path.join(OUTPUT_DIR, filename)
60
+
61
+ cv2.imwrite(save_path, crop)
62
+
63
+ labels.append({
64
+ "image": filename,
65
+ "predicate": item["predicate"]
66
+ })
67
+
68
+ idx += 1
69
+
70
+ with open(LABELS_PATH, "w") as f:
71
+ json.dump(labels, f)
72
+
73
+ print("Total processed:", idx)
src/config.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MAX_OBJECTS = 5
2
+ CONF_THRESHOLD = 0.7
3
+
4
+ RELATIONS = [
5
+ "on",
6
+ "next_to",
7
+ "holding",
8
+ "riding",
9
+ "behind",
10
+ "in_front_of",
11
+ "under"
12
+ ]
src/dataset.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import cv2
4
+ import torch
5
+ from torch.utils.data import Dataset
6
+
7
+ class RelationshipDataset(Dataset):
8
+ def __init__(self, image_dir, label_path):
9
+ self.image_dir = image_dir
10
+
11
+ with open(label_path) as f:
12
+ self.data = json.load(f)
13
+
14
+ def __len__(self):
15
+ return len(self.data)
16
+
17
+ def __getitem__(self, idx):
18
+ item = self.data[idx]
19
+
20
+ img_path = os.path.join(self.image_dir, item["image"])
21
+ image = cv2.imread(img_path)
22
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
23
+ image = image / 255.0
24
+ image = (image - 0.5) / 0.5
25
+
26
+ image = torch.tensor(image, dtype=torch.float32).permute(2, 0, 1)
27
+
28
+ label = torch.tensor(item["label"], dtype=torch.long)
29
+
30
+ return image, label
src/detection.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import DetrImageProcessor, DetrForObjectDetection
2
+ import torch
3
+ import cv2
4
+
5
+ device = "mps" if torch.backends.mps.is_available() else "cpu"
6
+
7
+ processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
8
+ model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
9
+ model.to(device)
10
+ model.eval()
11
+
12
+ id2label = model.config.id2label
13
+
14
+
15
+ def detect(image):
16
+ inputs = processor(images=image, return_tensors="pt").to(device)
17
+
18
+ with torch.no_grad():
19
+ outputs = model(**inputs)
20
+
21
+ target_sizes = torch.tensor([image.shape[:2]]).to(device)
22
+ results = processor.post_process_object_detection(outputs, target_sizes=target_sizes)[0]
23
+
24
+ from src.config import MAX_OBJECTS, CONF_THRESHOLD
25
+
26
+ detections = []
27
+
28
+ for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
29
+ if score.item() > CONF_THRESHOLD:
30
+ detections.append({
31
+ "label": id2label[label.item()],
32
+ "score": score.item(),
33
+ "box": box.tolist()
34
+ })
35
+
36
+ detections = sorted(detections, key=lambda x: x["score"], reverse=True)[:MAX_OBJECTS]
37
+
38
+ return detections
src/encode_labels.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ LABELS_PATH = "data/relationship_dataset/labels.json"
4
+ OUTPUT_PATH = "data/relationship_dataset/labels_encoded.json"
5
+ MAP_PATH = "data/relationship_dataset/label_map.json"
6
+
7
+ with open(LABELS_PATH) as f:
8
+ labels = json.load(f)
9
+
10
+ predicates = sorted(list(set([item["predicate"] for item in labels])))
11
+
12
+ label_map = {p: i for i, p in enumerate(predicates)}
13
+
14
+ encoded = []
15
+
16
+ for item in labels:
17
+ encoded.append({
18
+ "image": item["image"],
19
+ "label": label_map[item["predicate"]]
20
+ })
21
+
22
+ with open(OUTPUT_PATH, "w") as f:
23
+ json.dump(encoded, f)
24
+
25
+ with open(MAP_PATH, "w") as f:
26
+ json.dump(label_map, f)
27
+
28
+ print("Classes:", len(label_map))
src/model.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn as nn
2
+
3
+ class RelationshipNet(nn.Module):
4
+ def __init__(self, num_classes):
5
+ super().__init__()
6
+
7
+ self.model = nn.Sequential(
8
+ nn.Conv2d(3, 32, 3, padding=1),
9
+ nn.ReLU(),
10
+ nn.MaxPool2d(2),
11
+
12
+ nn.Conv2d(32, 64, 3, padding=1),
13
+ nn.ReLU(),
14
+ nn.MaxPool2d(2),
15
+
16
+ nn.Conv2d(64, 128, 3, padding=1),
17
+ nn.ReLU(),
18
+ nn.MaxPool2d(2),
19
+
20
+ nn.Flatten(),
21
+ nn.Linear(128 * 16 * 16, 256),
22
+ nn.ReLU(),
23
+ nn.Linear(256, num_classes)
24
+ )
25
+
26
+ def forward(self, x):
27
+ return self.model(x)
src/pipeline.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ from itertools import combinations
3
+
4
+ from src.detection import detect
5
+ from src.relationship_infer import predict
6
+ from src.spatial_rules import get_relation
7
+
8
+
9
+ def run_pipeline(image):
10
+
11
+ detections = detect(image)
12
+
13
+ results = []
14
+ pair_seen = set()
15
+
16
+ for obj1, obj2 in combinations(detections, 2):
17
+
18
+ # 🔥 avoid duplicate object pairs (by label)
19
+ pair_key = (obj1["label"], obj2["label"])
20
+ if pair_key in pair_seen:
21
+ continue
22
+ pair_seen.add(pair_key)
23
+
24
+ # boxes: [x1, y1, x2, y2]
25
+ x1, y1, x2, y2 = obj1["box"]
26
+ x3, y3, x4, y4 = obj2["box"]
27
+
28
+ # convert to (x, y, w, h)
29
+ box1 = (x1, y1, x2 - x1, y2 - y1)
30
+ box2 = (x3, y3, x4 - x3, y4 - y3)
31
+
32
+ # 🔥 spatial relation (primary)
33
+ spatial_rel = get_relation(box1, box2)
34
+
35
+ # crop region for model
36
+ x_min = int(min(x1, x3))
37
+ y_min = int(min(y1, y3))
38
+ x_max = int(max(x2, x4))
39
+ y_max = int(max(y2, y4))
40
+
41
+ crop = image[y_min:y_max, x_min:x_max]
42
+
43
+ if crop.size == 0:
44
+ continue
45
+
46
+ # 🔥 model prediction (secondary)
47
+ try:
48
+ model_rel = predict(crop)
49
+ except:
50
+ model_rel = None
51
+
52
+ # 🔥 hybrid logic
53
+ if model_rel in ["holding", "sitting_on"]:
54
+ relation = model_rel
55
+ else:
56
+ relation = spatial_rel
57
+
58
+ results.append({
59
+ "subject": obj1["label"],
60
+ "object": obj2["label"],
61
+ "relation": relation
62
+ })
63
+
64
+ # 🔥 remove duplicate relations
65
+ unique = set()
66
+ clean_results = []
67
+
68
+ for r in results:
69
+ key = (r["subject"], r["object"], r["relation"])
70
+ if key not in unique:
71
+ unique.add(key)
72
+ clean_results.append(r)
73
+
74
+ # 🔥 limit number of relations (clean graph)
75
+ MAX_RELATIONS = 8
76
+ clean_results = clean_results[:MAX_RELATIONS]
77
+
78
+ return clean_results
src/prepare_images.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ INPUT_PATH = "data/relationship_dataset/subset.json"
5
+ OUTPUT_PATH = "data/relationship_dataset/image_paths.json"
6
+
7
+ IMAGE_DIR_1 = "data/visual_genome/images/VG_100K"
8
+ IMAGE_DIR_2 = "data/visual_genome/images2/VG_100K_2"
9
+
10
+ with open(INPUT_PATH) as f:
11
+ data = json.load(f)
12
+
13
+ image_ids = set([item["image_id"] for item in data])
14
+
15
+ image_map = {}
16
+
17
+ for img_id in image_ids:
18
+ filename = f"{img_id}.jpg"
19
+
20
+ path1 = os.path.join(IMAGE_DIR_1, filename)
21
+ path2 = os.path.join(IMAGE_DIR_2, filename)
22
+
23
+ if os.path.exists(path1):
24
+ image_map[img_id] = path1
25
+ elif os.path.exists(path2):
26
+ image_map[img_id] = path2
27
+
28
+ with open(OUTPUT_PATH, "w") as f:
29
+ json.dump(image_map, f)
30
+
31
+ print("Total images found:", len(image_map))
src/relationship_dataset.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import random
3
+ from tqdm import tqdm
4
+ from src.config import RELATIONS
5
+
6
+ INPUT_PATH = "data/visual_genome/region_graphs.json"
7
+ OUTPUT_PATH = "data/relationship_dataset/subset.json"
8
+
9
+ subset_size = 10000
10
+ def normalize_predicate(p):
11
+ if "on" in p:
12
+ return "on"
13
+ if "next_to" in p or "next" in p:
14
+ return "next_to"
15
+ if "hold" in p:
16
+ return "holding"
17
+ if "ride" in p:
18
+ return "riding"
19
+ if "behind" in p:
20
+ return "behind"
21
+ if "front" in p:
22
+ return "in_front_of"
23
+ if "under" in p:
24
+ return "under"
25
+ return None
26
+
27
+ with open(INPUT_PATH) as f:
28
+ data = json.load(f)
29
+
30
+ valid_samples = []
31
+
32
+ for item in tqdm(data):
33
+ image_id = item["image_id"]
34
+
35
+ for region in item.get("regions", []):
36
+ objects = region.get("objects", [])
37
+ obj_map = {obj["object_id"]: obj for obj in objects}
38
+
39
+ for rel in region.get("relationships", []):
40
+ predicate = rel.get("predicate", "").lower().replace(" ", "_")
41
+
42
+ normalized = normalize_predicate(predicate)
43
+
44
+ if normalized is not None:
45
+ subject_id = rel.get("subject_id")
46
+ object_id = rel.get("object_id")
47
+
48
+ if subject_id in obj_map and object_id in obj_map:
49
+ subject = obj_map[subject_id]
50
+ obj = obj_map[object_id]
51
+
52
+ valid_samples.append({
53
+ "image_id": image_id,
54
+ "predicate": normalized,
55
+ "subject": subject,
56
+ "object": obj
57
+ })
58
+
59
+ random.shuffle(valid_samples)
60
+
61
+ subset = valid_samples[:subset_size]
62
+
63
+ with open(OUTPUT_PATH, "w") as f:
64
+ json.dump(subset, f)
65
+
66
+ print("Total samples:", len(subset))
src/relationship_infer.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import cv2
3
+ import json
4
+ from src.model import RelationshipNet
5
+
6
+ MODEL_PATH = "models/relationship_model.pth"
7
+ LABEL_MAP_PATH = "data/relationship_dataset/label_map.json"
8
+
9
+ device = "mps" if torch.backends.mps.is_available() else "cpu"
10
+
11
+ with open(LABEL_MAP_PATH) as f:
12
+ label_map = json.load(f)
13
+
14
+ inv_map = {v: k for k, v in label_map.items()}
15
+
16
+ num_classes = len(label_map)
17
+
18
+ model = RelationshipNet(num_classes)
19
+ model.load_state_dict(torch.load(MODEL_PATH, map_location=device))
20
+ model.to(device)
21
+ model.eval()
22
+
23
+
24
+ def predict(image):
25
+ image = cv2.resize(image, (128, 128))
26
+ image = image / 255.0
27
+ image = (image - 0.5) / 0.5
28
+
29
+ image = torch.tensor(image, dtype=torch.float32).permute(2, 0, 1)
30
+ image = image.unsqueeze(0).to(device)
31
+
32
+ with torch.no_grad():
33
+ output = model(image)
34
+ pred = torch.argmax(output, dim=1).item()
35
+
36
+ return inv_map[pred]
src/scene_graph.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import networkx as nx
2
+
3
+
4
+ def build_graph(relations):
5
+ G = nx.DiGraph()
6
+
7
+ seen = set()
8
+
9
+ for rel in relations:
10
+ subj = rel["subject"]
11
+ obj = rel["object"]
12
+ predicate = rel["relation"]
13
+
14
+ key = (subj, obj, predicate)
15
+
16
+
17
+ if key in seen:
18
+ continue
19
+ seen.add(key)
20
+
21
+ G.add_node(subj)
22
+ G.add_node(obj)
23
+ G.add_edge(subj, obj, label=predicate)
24
+
25
+ return G
src/spatial_rules.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def get_relation(subj_box, obj_box):
2
+ sx, sy, sw, sh = subj_box
3
+ ox, oy, ow, oh = obj_box
4
+
5
+ # centers
6
+ scx, scy = sx + sw / 2, sy + sh / 2
7
+ ocx, ocy = ox + ow / 2, oy + oh / 2
8
+
9
+ # vertical relation
10
+ if scy < ocy - 20:
11
+ return "above"
12
+ elif scy > ocy + 20:
13
+ return "below"
14
+
15
+ # horizontal relation
16
+ if scx < ocx - 20:
17
+ return "left_of"
18
+ elif scx > ocx + 20:
19
+ return "right_of"
20
+
21
+ return "near"
src/text_generation.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
2
+
3
+ tokenizer = AutoTokenizer.from_pretrained("google/flan-t5-small")
4
+ model = AutoModelForSeq2SeqLM.from_pretrained("google/flan-t5-small")
5
+
6
+
7
+ def graph_to_text(relations):
8
+ if not relations:
9
+ return "No relationships detected."
10
+
11
+ # convert relations to prompt
12
+ triples = [
13
+ f"{r['subject']} {r['relation']} {r['object']}"
14
+ for r in relations
15
+ ]
16
+
17
+ prompt = "Convert the following relationships into a natural sentence:\n"
18
+ prompt += ", ".join(triples)
19
+
20
+ inputs = tokenizer(prompt, return_tensors="pt", truncation=True)
21
+
22
+ outputs = model.generate(**inputs, max_length=50)
23
+
24
+ return tokenizer.decode(outputs[0], skip_special_tokens=True)
src/visualization.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import plotly.graph_objects as go
2
+ import networkx as nx
3
+
4
+
5
+ def visualize_graph(G):
6
+ pos = nx.spring_layout(G, seed=42)
7
+
8
+ edge_x = []
9
+ edge_y = []
10
+ edge_text = []
11
+
12
+ for u, v, data in G.edges(data=True):
13
+ x0, y0 = pos[u]
14
+ x1, y1 = pos[v]
15
+
16
+ edge_x += [x0, x1, None]
17
+ edge_y += [y0, y1, None]
18
+ edge_text.append(data["label"])
19
+
20
+ edge_trace = go.Scatter(
21
+ x=edge_x,
22
+ y=edge_y,
23
+ line=dict(width=1),
24
+ hoverinfo='none',
25
+ mode='lines'
26
+ )
27
+
28
+ node_x = []
29
+ node_y = []
30
+ node_text = []
31
+
32
+ for node in G.nodes():
33
+ x, y = pos[node]
34
+ node_x.append(x)
35
+ node_y.append(y)
36
+ node_text.append(node)
37
+
38
+ node_trace = go.Scatter(
39
+ x=node_x,
40
+ y=node_y,
41
+ mode='markers+text',
42
+ text=node_text,
43
+ textposition="top center",
44
+ hoverinfo='text',
45
+ marker=dict(size=20)
46
+ )
47
+
48
+ fig = go.Figure(data=[edge_trace, node_trace])
49
+
50
+ fig.update_layout(
51
+ showlegend=False,
52
+ hovermode='closest',
53
+ margin=dict(b=20, l=5, r=5, t=40)
54
+ )
55
+ for u, v in G.edges():
56
+ x0, y0 = pos[u]
57
+ x1, y1 = pos[v]
58
+
59
+ fig.add_annotation(
60
+ x=x1,
61
+ y=y1,
62
+ ax=x0,
63
+ ay=y0,
64
+ xref='x',
65
+ yref='y',
66
+ axref='x',
67
+ ayref='y',
68
+ showarrow=True,
69
+
70
+ arrowhead=4,
71
+ arrowsize=2.5,
72
+ arrowwidth=2.5,
73
+ arrowcolor="black",
74
+
75
+ opacity=0.9
76
+ )
77
+
78
+ return fig
training/train_relationship.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch.utils.data import DataLoader
3
+ from src.dataset import RelationshipDataset
4
+ from src.model import RelationshipNet
5
+
6
+ device = "mps" if torch.backends.mps.is_available() else "cpu"
7
+
8
+ dataset = RelationshipDataset(
9
+ image_dir="data/relationship_dataset/images",
10
+ label_path="data/relationship_dataset/labels_encoded.json"
11
+ )
12
+
13
+ loader = DataLoader(dataset, batch_size=16, shuffle=True)
14
+
15
+ num_classes = len(set([item["label"] for item in dataset.data]))
16
+
17
+ model = RelationshipNet(num_classes).to(device)
18
+
19
+ criterion = torch.nn.CrossEntropyLoss()
20
+ optimizer = torch.optim.Adam(model.parameters(), lr=3e-4)
21
+
22
+ epochs = 10
23
+
24
+ for epoch in range(epochs):
25
+ total_loss = 0
26
+
27
+ for images, labels in loader:
28
+ images = images.to(device)
29
+ labels = labels.to(device)
30
+
31
+ outputs = model(images)
32
+ loss = criterion(outputs, labels)
33
+
34
+ optimizer.zero_grad()
35
+ loss.backward()
36
+ optimizer.step()
37
+
38
+ total_loss += loss.item()
39
+
40
+ print(f"Epoch {epoch+1}, Loss: {total_loss:.4f}")
41
+
42
+ torch.save(model.state_dict(), "models/relationship_model.pth")