Spaces:
Running
Running
Kalp Kanungo commited on
Commit ·
c858478
0
Parent(s):
Initial commit - Multimodal AI project
Browse files- .gitignore +44 -0
- README.md +152 -0
- app.py +53 -0
- requirements.txt +16 -0
- src/__init__.py +0 -0
- src/build_relationship_dataset.py +73 -0
- src/config.py +12 -0
- src/dataset.py +30 -0
- src/detection.py +38 -0
- src/encode_labels.py +28 -0
- src/model.py +27 -0
- src/pipeline.py +78 -0
- src/prepare_images.py +31 -0
- src/relationship_dataset.py +66 -0
- src/relationship_infer.py +36 -0
- src/scene_graph.py +25 -0
- src/spatial_rules.py +21 -0
- src/text_generation.py +24 -0
- src/visualization.py +78 -0
- training/train_relationship.py +42 -0
.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")
|