Pradana Yahya Abdillah commited on
Commit
cee2dce
·
1 Parent(s): 8ed47f0
Files changed (7) hide show
  1. .gitignore +69 -0
  2. app.py +104 -0
  3. pipeline.py +501 -0
  4. requirements.txt +12 -0
  5. utils/__init__.py +0 -0
  6. utils/metrics.py +121 -0
  7. utils/visualization.py +114 -0
.gitignore ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model files
2
+ *.pt
3
+ *.pth
4
+ *.bin
5
+ *.onnx
6
+ *.ckpt
7
+ *.safetensors
8
+ models/
9
+ **/models/
10
+ yolo11s.pt
11
+ **/shelf_model/
12
+
13
+ # Hugging Face cache
14
+ .cache/
15
+ .huggingface/
16
+
17
+ # # Output files
18
+ # outputs/
19
+ # **/outputs/
20
+ # *.mp4
21
+ # *.avi
22
+ # *.png
23
+ # *.jpg
24
+ # *.jpeg
25
+ # *.csv
26
+ # !requirements.txt
27
+
28
+ # Python
29
+ __pycache__/
30
+ *.py[cod]
31
+ *$py.class
32
+ *.so
33
+ .Python
34
+ env/
35
+ build/
36
+ develop-eggs/
37
+ dist/
38
+ downloads/
39
+ eggs/
40
+ .eggs/
41
+ lib/
42
+ lib64/
43
+ parts/
44
+ sdist/
45
+ var/
46
+ *.egg-info/
47
+ .installed.cfg
48
+ *.egg
49
+
50
+ # Virtual environments
51
+ venv/
52
+ env/
53
+ ENV/
54
+ .env
55
+ .venv
56
+
57
+ # IDE specific files
58
+ .idea/
59
+ .vscode/
60
+ *.swp
61
+ *.swo
62
+ .DS_Store
63
+
64
+ # Jupyter Notebook
65
+ .ipynb_checkpoints
66
+
67
+ # Logs
68
+ *.log
69
+ logs/
app.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import tempfile
4
+ import time
5
+ from pipeline import full_video_analysis, get_key_metrics
6
+
7
+ # Add at the top with other imports
8
+ import shutil
9
+
10
+ # Add this after your other imports
11
+ OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs")
12
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
13
+
14
+ # Cache model loading
15
+ model_cache = {}
16
+
17
+ def process_video(video_file, max_duration=30):
18
+ """Process video and return analysis results"""
19
+ if video_file is None:
20
+ return None, None, None, None, None
21
+
22
+ # Create temp directory for outputs
23
+ with tempfile.TemporaryDirectory() as temp_dir:
24
+ # Gradio sometimes uploads video as .mp4 or as .webm
25
+ temp_video = os.path.join(temp_dir, "input.mp4")
26
+
27
+ # Handle both string paths and file-like objects
28
+ if isinstance(video_file, str):
29
+ with open(temp_video, "wb") as f:
30
+ f.write(open(video_file, "rb").read())
31
+ else:
32
+ # Handle file-like object with .name attribute
33
+ with open(temp_video, "wb") as f:
34
+ f.write(open(video_file.name, "rb").read())
35
+
36
+ # Process the video
37
+ try:
38
+ processed_video, metrics = full_video_analysis(temp_video, temp_dir, max_duration=max_duration)
39
+
40
+ # Generate visualizations from metrics
41
+ heatmap, dwell_chart, action_chart, archetype_table = get_key_metrics(temp_dir)
42
+
43
+ # After processing, copy important files to persistent location
44
+ persistent_video_path = os.path.join(OUTPUT_DIR, f"video_output_{int(time.time())}.mp4")
45
+ persistent_heatmap_path = os.path.join(OUTPUT_DIR, f"heatmap_{int(time.time())}.png")
46
+ persistent_dwell_path = os.path.join(OUTPUT_DIR, f"dwell_{int(time.time())}.png")
47
+ persistent_journey_path = os.path.join(OUTPUT_DIR, f"journey_{int(time.time())}.png")
48
+
49
+ shutil.copy(processed_video, persistent_video_path)
50
+ shutil.copy(metrics['heatmap'], persistent_heatmap_path)
51
+ shutil.copy(metrics['dwell_time'], persistent_dwell_path)
52
+ shutil.copy(metrics['journey'], persistent_journey_path)
53
+
54
+ # In process_video function after copying files
55
+ return persistent_video_path, persistent_heatmap_path, persistent_dwell_path, persistent_journey_path, archetype_table
56
+ # return processed_video, heatmap, dwell_chart, action_chart, archetype_table
57
+
58
+ except Exception as e:
59
+ # Return appropriate data types for each output
60
+ error_message = f"Error processing video: {str(e)}"
61
+ print(error_message) # For debugging
62
+ return None, None, None, None, None # Return None for all outputs
63
+
64
+ # Define Gradio Interface
65
+ with gr.Blocks(title="Supermarket Behaviour Analysis") as demo:
66
+ gr.Markdown("# 🛒 Supermarket Behaviour Analysis")
67
+ gr.Markdown("""
68
+ Upload a video of people shopping in a supermarket, and the AI will:
69
+ 1. Detect shelves and people
70
+ 2. Track people movement
71
+ 3. Classify their actions
72
+ 4. Analyze shelf interactions
73
+ 5. Produce insights on shelf effectiveness
74
+ """)
75
+
76
+ with gr.Row():
77
+ with gr.Column(scale=1):
78
+ input_video = gr.Video(label="Upload Video (max 30s)")
79
+ duration_slider = gr.Slider(1, 60, value=30, step=1, label="Max Duration (seconds)")
80
+ submit_btn = gr.Button("Analyze", variant="primary")
81
+
82
+ with gr.Column(scale=2):
83
+ output_video = gr.Video(label="Processed Video")
84
+
85
+ with gr.Row():
86
+ with gr.Column():
87
+ heatmap_img = gr.Image(label="Customer Traffic Heatmap")
88
+ with gr.Column():
89
+ dwell_chart = gr.Image(label="Dwell Time per Shelf (seconds)")
90
+
91
+ with gr.Row():
92
+ with gr.Column():
93
+ action_chart = gr.Image(label="Customer Journey Analysis")
94
+ with gr.Column():
95
+ archetype_table = gr.DataFrame(label="Shelf Behavioral Archetypes")
96
+
97
+ submit_btn.click(
98
+ process_video,
99
+ inputs=[input_video, duration_slider],
100
+ outputs=[output_video, heatmap_img, dwell_chart, action_chart, archetype_table]
101
+ )
102
+
103
+ if __name__ == "__main__":
104
+ demo.launch()
pipeline.py ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import torch
4
+ import numpy as np
5
+ import pandas as pd
6
+ import matplotlib.pyplot as plt
7
+ from collections import defaultdict
8
+ from decord import VideoReader, cpu
9
+ from ultralytics import YOLO
10
+ from transformers import AutoImageProcessor, AutoModelForVideoClassification
11
+ import supervision as sv
12
+ from shapely.geometry import box as shp_box
13
+ from huggingface_hub import snapshot_download
14
+
15
+ # Add at the top with other imports
16
+ import shutil
17
+
18
+ # Add this after your other imports
19
+ OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs")
20
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
21
+
22
+ # Global model cache
23
+ MODELS = {}
24
+
25
+ def load_models():
26
+ """Load all required models"""
27
+ global MODELS
28
+
29
+ if not MODELS:
30
+ print("Loading models...")
31
+
32
+ # Set device
33
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
34
+ print(f"Using device: {device}")
35
+ if torch.cuda.is_available():
36
+ print(f"GPU: {torch.cuda.get_device_name(0)}")
37
+
38
+ # Download shelf segmentation model
39
+ snapshot_download(repo_id="cheesecz/shelf-segmentation", local_dir="models/shelf_model", local_dir_use_symlinks=False)
40
+
41
+ # Load models with explicit device setting
42
+ MODELS["person_model"] = YOLO('yolo11s.pt').to(device)
43
+ MODELS["shelf_model"] = YOLO("models/shelf_model/best.pt").to(device)
44
+ MODELS["action_model"] = AutoModelForVideoClassification.from_pretrained('haipradana/s-h-o-p-domain-adaptation').to(device)
45
+ MODELS["image_processor"] = AutoImageProcessor.from_pretrained('haipradana/s-h-o-p-domain-adaptation')
46
+
47
+ # Store device info
48
+ MODELS["device"] = device
49
+ MODELS["action_model"].eval() # Set model to evaluation mode
50
+ MODELS["id2label"] = MODELS["action_model"].config.id2label
51
+
52
+ print("Models loaded successfully")
53
+
54
+ return MODELS
55
+
56
+ def merge_consecutive_predictions(preds, min_duration_frames=0):
57
+ """Merge consecutive predictions of the same class"""
58
+ if not preds: return []
59
+ merged = []
60
+ current = preds[0].copy()
61
+ for nxt in preds[1:]:
62
+ if nxt['pred'] == current['pred']:
63
+ current['end'] = nxt['end']
64
+ else:
65
+ merged.append(current)
66
+ current = nxt.copy()
67
+ merged.append(current)
68
+ return [e for e in merged if (e['end'] - e['start']) >= min_duration_frames]
69
+
70
+ def iou_xyxy(a, b):
71
+ """Calculate IoU between two bounding boxes in (x1,y1,x2,y2) format"""
72
+ inter = shp_box(*a).intersection(shp_box(*b)).area
73
+ union = shp_box(*a).union(shp_box(*b)).area
74
+ return inter / union if union else 0
75
+
76
+ def full_video_analysis(video_path, output_dir, max_duration=30):
77
+ """
78
+ Process a video file and generate analysis outputs
79
+
80
+ Args:
81
+ video_path: Path to input video
82
+ output_dir: Directory to save outputs
83
+ max_duration: Maximum video duration to process (in seconds)
84
+
85
+ Returns:
86
+ Path to processed video
87
+ Dictionary of metrics
88
+ """
89
+ # Load models if not already loaded
90
+ models = load_models()
91
+ person_model = models["person_model"]
92
+ shelf_model = models["shelf_model"]
93
+ action_model = models["action_model"]
94
+ image_processor = models["image_processor"]
95
+ device = models["device"]
96
+ id2label = models["id2label"]
97
+
98
+ # Load video
99
+ vr = VideoReader(video_path, ctx=cpu(0))
100
+ fps = vr.get_avg_fps()
101
+ print(f"FPS video = {fps:.2f}")
102
+ H, W, _ = vr[0].shape
103
+
104
+ # Limit video length
105
+ max_frames = min(len(vr), int(max_duration * fps))
106
+
107
+ # Output video path
108
+ out_path = os.path.join(output_dir, 'video_output.mp4')
109
+ vw = cv2.VideoWriter(out_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (W, H))
110
+
111
+ # Initialize tracking
112
+ # Replace the tracker initialization with:
113
+ # tracker = person_model.track(source=video_path, persist=True, tracker='bytetrack.yaml',
114
+ # classes=[0], stream=True)
115
+
116
+ tracker = person_model.track(source=video_path, persist=True, tracker='bytetrack.yaml',
117
+ classes=[0], stream=True, device=device)
118
+
119
+ # Rest of your frame processing code
120
+ # Data containers
121
+ tracks = defaultdict(list)
122
+ raw_actions = defaultdict(list)
123
+ heatmap_grid = np.zeros((20, 20))
124
+ shelf_boxes_per_frame = {}
125
+ shelf_last_box = {}
126
+ next_shelf_idx = 1
127
+ IOU_TH = 0.5 # IoU threshold for considering same shelf
128
+
129
+ # ---------- PASS 1: Detection + Tracking ----------
130
+ # Then limit frames manually in your processing loop
131
+ for f_idx, result in enumerate(tracker):
132
+ if f_idx >= max_frames:
133
+ break
134
+
135
+ frame = vr[f_idx].asnumpy()
136
+ # res_shelf = shelf_model(frame)
137
+ res_shelf = shelf_model(frame, device=device)
138
+
139
+ # Process shelf detections
140
+ assigned = []
141
+ raw_boxes = [b.xyxy[0].cpu().numpy() for b in res_shelf[0].boxes] if res_shelf[0].boxes else []
142
+
143
+ for box in raw_boxes:
144
+ cur = tuple(map(int, box))
145
+ best_iou, best_id = 0, None
146
+ for sid, prev in shelf_last_box.items():
147
+ val = iou_xyxy(cur, prev)
148
+ if val > best_iou:
149
+ best_iou, best_id = val, sid
150
+ if best_iou >= IOU_TH:
151
+ shelf_last_box[best_id] = cur
152
+ assigned.append((best_id, cur))
153
+ else:
154
+ sid = f"shelf_{next_shelf_idx}"
155
+ next_shelf_idx += 1
156
+ shelf_last_box[sid] = cur
157
+ assigned.append((sid, cur))
158
+
159
+ shelf_boxes_per_frame[f_idx] = assigned
160
+
161
+ # Process person detections
162
+ if result.boxes.id is not None:
163
+ boxes = result.boxes.xyxy.cpu().numpy()
164
+ ids = result.boxes.id.int().cpu().tolist()
165
+ for box, pid in zip(boxes, ids):
166
+ tracks[pid].append({'frame': f_idx, 'bbox': box, 'pid': pid})
167
+ cx, cy = (box[0] + box[2])/2, (box[1] + box[3])/2
168
+ gx, gy = min(int(cx/W*20), 19), min(int(cy/H*20), 19)
169
+ heatmap_grid[gy, gx] += 1
170
+
171
+ # ---------- Action Recognition ----------
172
+ for pid, dets in tracks.items():
173
+ if len(dets) < 16: continue
174
+ for i in range(0, len(dets)-15, 8):
175
+ clip_frames = [d['frame'] for d in dets[i:i+16]]
176
+ imgs = vr.get_batch(clip_frames).asnumpy()
177
+ crops = [img[int(d['bbox'][1]):int(d['bbox'][3]),
178
+ int(d['bbox'][0]):int(d['bbox'][2])] for img, d in zip(imgs, dets[i:i+16])]
179
+ if not crops: continue
180
+ try:
181
+ inp = image_processor(crops, return_tensors='pt').to(device)
182
+ pred = action_model(**inp).logits.argmax(-1).item()
183
+ raw_actions[pid].append({'start': dets[i]['frame'], 'end': dets[i+15]['frame'], 'pred': pred})
184
+ except Exception as e:
185
+ print(f"Error processing action for pid {pid}: {e}")
186
+
187
+ action_preds = {pid: merge_consecutive_predictions(v, int(fps*0.4))
188
+ for pid, v in raw_actions.items()}
189
+
190
+ # ---------- Calculate Shelf Interactions ----------
191
+ shelf_interaksi = defaultdict(int)
192
+ for pid, dets in tracks.items():
193
+ for d in dets:
194
+ f = d['frame']
195
+ x1, y1, x2, y2 = d['bbox']
196
+ cx, cy = (x1+x2)/2, (y1+y2)/2
197
+ for sid, (sx1, sy1, sx2, sy2) in shelf_boxes_per_frame.get(f, []):
198
+ if sx1 <= cx <= sx2 and sy1 <= cy <= sy2:
199
+ shelf_interaksi[sid] += 1
200
+
201
+ # Save interaction summary
202
+ pd.DataFrame(list(shelf_interaksi.items()),
203
+ columns=['shelf_id', 'interaksi']).to_csv(
204
+ os.path.join(output_dir, 'rak_interaksi.csv'), index=False)
205
+
206
+ # ---------- Generate Heatmap ----------
207
+ plt.figure(figsize=(10, 8))
208
+ plt.imshow(heatmap_grid, cmap='hot', interpolation='nearest')
209
+ plt.title('Heatmap Flow Pengunjung')
210
+ plt.colorbar()
211
+ plt.tight_layout()
212
+ plt.savefig(os.path.join(output_dir, 'heatmap.png'))
213
+ plt.close()
214
+
215
+ # ---------- Action Summary ----------
216
+ all_actions = []
217
+ for pid, acts in action_preds.items():
218
+ for a in acts:
219
+ all_actions.append([pid, a['start'], a['end'], id2label[a['pred']]])
220
+
221
+ pd.DataFrame(all_actions,
222
+ columns=['id', 'start', 'end', 'action']).to_csv(
223
+ os.path.join(output_dir, 'action_log.csv'), index=False)
224
+
225
+ pd.DataFrame(pd.Series([row[3] for row in all_actions])
226
+ .value_counts()).to_csv(
227
+ os.path.join(output_dir, 'action_summary.csv'))
228
+
229
+ # ---------- Action ↔ Shelf Mapping ----------
230
+ action_shelf = []
231
+ shelf_action_counter = defaultdict(int)
232
+
233
+ for pid, acts in action_preds.items():
234
+ for seg in acts:
235
+ s, e, act_id = seg['start'], seg['end'], seg['pred']
236
+ act_label = id2label[act_id]
237
+
238
+ for f in range(s, e+1):
239
+ det = next((d for d in tracks[pid] if d['frame'] == f), None)
240
+ if det is None: continue
241
+ x1, y1, x2, y2 = det['bbox']
242
+ cx, cy = (x1+x2)/2, (y1+y2)/2
243
+
244
+ for sid, (sx1, sy1, sx2, sy2) in shelf_boxes_per_frame.get(f, []):
245
+ if sx1 <= cx <= sx2 and sy1 <= cy <= sy2:
246
+ action_shelf.append([pid, f, sid, act_label])
247
+ shelf_action_counter[(sid, act_label)] += 1
248
+ break
249
+
250
+ # Save detailed action-shelf mapping
251
+ pd.DataFrame(action_shelf,
252
+ columns=['pid', 'frame', 'shelf_id', 'action']).to_csv(
253
+ os.path.join(output_dir, 'action_shelf_log.csv'), index=False)
254
+
255
+ pd.DataFrame([{'shelf_id': k[0], 'action': k[1], 'count': v}
256
+ for k, v in shelf_action_counter.items()]).to_csv(
257
+ os.path.join(output_dir, 'action_shelf_summary.csv'), index=False)
258
+
259
+ # ---------- Layout Recommendations ----------
260
+ pd.DataFrame(sorted(shelf_interaksi.items(),
261
+ key=lambda x: -x[1]),
262
+ columns=['shelf_id', 'interaksi']).to_csv(
263
+ os.path.join(output_dir, 'rekomendasi_layout.csv'), index=False)
264
+
265
+ # ---------- Create Video with Overlay ----------
266
+ heatmap_ann = sv.HeatMapAnnotator(position=sv.Position.BOTTOM_CENTER,
267
+ opacity=0.3, radius=20, kernel_size=25)
268
+
269
+ # For web performance, we can skip frames if needed
270
+ render_every = max(1, int(len(vr) / 300)) # Aim for ~300 frames max
271
+
272
+ for f_idx in range(min(max_frames, len(vr))):
273
+ if f_idx % render_every != 0 and f_idx != min(max_frames, len(vr))-1: # Always render last frame
274
+ continue
275
+
276
+ frame = vr[f_idx].asnumpy()
277
+ frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
278
+
279
+ # Draw shelves
280
+ for sid, (x1, y1, x2, y2) in shelf_boxes_per_frame.get(f_idx, []):
281
+ cv2.rectangle(frame_bgr, (x1, y1), (x2, y2), (255, 0, 0), 2)
282
+ cv2.putText(frame_bgr, sid, (x1, y1-5),
283
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
284
+
285
+ # Draw persons
286
+ cur_tracks = [t for pid, v in tracks.items() for t in v if t['frame'] == f_idx]
287
+ for t in cur_tracks:
288
+ x1, y1, x2, y2 = map(int, t['bbox'])
289
+ pid = t['pid']
290
+ label = f"ID {pid}"
291
+ for a in action_preds.get(pid, []):
292
+ if a['start'] <= f_idx <= a['end']:
293
+ label += f" | {id2label[a['pred']]}"
294
+ break
295
+ cv2.rectangle(frame_bgr, (x1, y1), (x2, y2), (0, 255, 0), 2)
296
+ cv2.putText(frame_bgr, label, (x1, y1-10),
297
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
298
+
299
+
300
+ # Add heatmap if there are tracks
301
+ cur_tracks = [t for pid, v in tracks.items() for t in v if t['frame'] == f_idx]
302
+ if cur_tracks:
303
+ dets = sv.Detections(xyxy=np.array([t['bbox'] for t in cur_tracks]),
304
+ confidence=np.ones(len(cur_tracks)),
305
+ class_id=np.zeros(len(cur_tracks)))
306
+ frame_bgr = heatmap_ann.annotate(scene=frame_bgr.copy(), detections=dets)
307
+
308
+ vw.write(frame_bgr)
309
+
310
+ vw.release()
311
+
312
+ # Generate additional analytics
313
+ generate_dwell_time_analysis(os.path.join(output_dir, 'action_shelf_log.csv'),
314
+ output_dir, fps)
315
+ generate_journey_analysis(os.path.join(output_dir, 'action_shelf_log.csv'),
316
+ output_dir)
317
+ generate_behavioral_archetypes(output_dir)
318
+
319
+ # Return paths and metrics
320
+ return out_path, {
321
+ 'heatmap': os.path.join(output_dir, 'heatmap.png'),
322
+ 'dwell_time': os.path.join(output_dir, 'dwell_time_chart.png'),
323
+ 'journey': os.path.join(output_dir, 'journey_chart.png'),
324
+ 'archetypes': os.path.join(output_dir, 'behavioral_archetypes.csv')
325
+ }
326
+
327
+ def generate_dwell_time_analysis(action_shelf_log_path, output_dir, fps):
328
+ """Generate dwell time analysis chart"""
329
+ df = pd.read_csv(action_shelf_log_path)
330
+
331
+ # Calculate dwell time
332
+ dwell_time_data = []
333
+ for pid, group in df.groupby('pid'):
334
+ start_frame, current_shelf = 0, -1
335
+ for i, row in group.iterrows():
336
+ if row['shelf_id'] != current_shelf:
337
+ if current_shelf != -1:
338
+ dwell_seconds = (end_frame - start_frame) / fps
339
+ dwell_time_data.append({'pid': pid, 'shelf_id': current_shelf, 'dwell_time': dwell_seconds})
340
+ current_shelf, start_frame = row['shelf_id'], row['frame']
341
+ end_frame = row['frame']
342
+ if current_shelf != -1:
343
+ dwell_seconds = (end_frame - start_frame) / fps
344
+ dwell_time_data.append({'pid': pid, 'shelf_id': current_shelf, 'dwell_time': dwell_seconds})
345
+
346
+ dwell_df = pd.DataFrame(dwell_time_data)
347
+ avg_dwell_time = dwell_df.groupby('shelf_id')['dwell_time'].mean().sort_values(ascending=False)
348
+
349
+ # Save data
350
+ avg_dwell_time.to_csv(os.path.join(output_dir, 'average_dwell_time.csv'))
351
+
352
+ # Create visualization
353
+ plt.figure(figsize=(10, 6))
354
+ avg_dwell_time.plot(kind='bar', color='purple')
355
+ plt.title('Dwell Time (Rata-rata) Tiap Rak', fontsize=14)
356
+ plt.xlabel('Shelf ID')
357
+ plt.ylabel('Seconds')
358
+ plt.tight_layout()
359
+ plt.savefig(os.path.join(output_dir, 'dwell_time_chart.png'))
360
+ plt.close()
361
+
362
+ def generate_journey_analysis(action_shelf_log_path, output_dir):
363
+ """Generate customer journey analysis chart"""
364
+ df = pd.read_csv(action_shelf_log_path)
365
+
366
+ # Find key events
367
+ reach_events = df[df['action'] == 'Reach To Shelf'][['pid', 'shelf_id']].drop_duplicates().assign(did_reach=True)
368
+ inspect_events = df[df['action'] == 'Inspect Product'][['pid', 'shelf_id']].drop_duplicates().assign(did_inspect=True)
369
+ return_events = df[df['action'] == 'Hand In Shelf'][['pid', 'shelf_id']].drop_duplicates().assign(did_return=True)
370
+
371
+ # Create analysis dataframe
372
+ interactions = df[['pid', 'shelf_id']].drop_duplicates()
373
+ analysis_df = pd.merge(interactions, reach_events, on=['pid', 'shelf_id'], how='left')
374
+ analysis_df = pd.merge(analysis_df, inspect_events, on=['pid', 'shelf_id'], how='left')
375
+ analysis_df = pd.merge(analysis_df, return_events, on=['pid', 'shelf_id'], how='left')
376
+ analysis_df = analysis_df.fillna(False)
377
+
378
+ # Categorize outcomes
379
+ def categorize_outcome(row):
380
+ if not row['did_reach']:
381
+ return 'No Reach'
382
+ if row['did_inspect'] and row['did_return']:
383
+ return 'Keraguan & Pembatalan'
384
+ elif row['did_inspect'] and not row['did_return']:
385
+ return 'Konversi Sukses'
386
+ else:
387
+ return 'Kegagalan Menarik Minat'
388
+
389
+ analysis_df['outcome'] = analysis_df.apply(categorize_outcome, axis=1)
390
+ relevant_outcomes = analysis_df[analysis_df['outcome'] != 'No Reach']
391
+
392
+ # Aggregate results
393
+ outcome_summary = relevant_outcomes.groupby(['shelf_id', 'outcome']).size().unstack(fill_value=0)
394
+ outcome_percentage = outcome_summary.div(outcome_summary.sum(axis=1), axis=0) * 100
395
+
396
+ desired_order = ['Konversi Sukses', 'Keraguan & Pembatalan', 'Kegagalan Menarik Minat']
397
+ for col in desired_order:
398
+ if col not in outcome_percentage.columns:
399
+ outcome_percentage[col] = 0
400
+ outcome_percentage = outcome_percentage[desired_order]
401
+
402
+ # Save data
403
+ outcome_percentage.to_csv(os.path.join(output_dir, 'journey_analysis.csv'))
404
+
405
+ # Create visualization
406
+ plt.figure(figsize=(12, 6))
407
+ outcome_percentage.plot(
408
+ kind='bar',
409
+ stacked=True,
410
+ color=['#2ca02c', '#ff7f0e', '#d62728'] # Green, Orange, Red
411
+ )
412
+ plt.title('Analisis Perilaku Pengunjung tiap Rak', fontsize=14)
413
+ plt.xlabel('Shelf ID')
414
+ plt.ylabel('Outcome Distribution (%)')
415
+ plt.legend(title='Interaction Outcome')
416
+ plt.tight_layout()
417
+ plt.savefig(os.path.join(output_dir, 'journey_chart.png'))
418
+ plt.close()
419
+
420
+ def generate_behavioral_archetypes(output_dir):
421
+ """Generate behavioral archetypes analysis"""
422
+ # Load previously calculated data
423
+ try:
424
+ df_raw = pd.read_csv(os.path.join(output_dir, 'action_shelf_log.csv'))
425
+ df_dwell = pd.read_csv(os.path.join(output_dir, 'average_dwell_time.csv'))
426
+ df_dwell.rename(columns={df_dwell.columns[0]: 'shelf_id'}, inplace=True)
427
+ df_outcomes = pd.read_csv(os.path.join(output_dir, 'journey_analysis.csv')).set_index('shelf_id')
428
+
429
+ # Calculate unique interactions
430
+ unique_interactions = df_raw.groupby('shelf_id')['pid'].nunique().reset_index()
431
+ unique_interactions.rename(columns={'pid': 'Interaksi Unik'}, inplace=True)
432
+
433
+ # Merge data
434
+ summary_table = pd.merge(unique_interactions, df_dwell, on='shelf_id')
435
+ summary_table = summary_table.set_index('shelf_id')
436
+
437
+ # Define behavioral archetypes
438
+ def get_behavioral_archetype(row):
439
+ shelf_id = row.name
440
+ dwell_time = row['dwell_time']
441
+ unique_visits = row['Interaksi Unik']
442
+
443
+ # Check if shelf exists in outcomes data
444
+ if shelf_id not in df_outcomes.index:
445
+ if dwell_time > 3.0:
446
+ return 'Passive Attention (No Physical Engagement)'
447
+ else:
448
+ return 'Low Engagement Zone'
449
+
450
+ outcomes = df_outcomes.loc[shelf_id]
451
+ if 'Konversi Sukses' in outcomes and outcomes['Konversi Sukses'] > 10:
452
+ return 'High Attention, Low Conversion'
453
+
454
+ if 'Keraguan & Pembatalan' in outcomes.index:
455
+ dominant_outcome = outcomes.idxmax()
456
+ if dominant_outcome == 'Keraguan & Pembatalan':
457
+ return 'Interaksi Positif Namun Ragu'
458
+
459
+ if unique_visits > 8:
460
+ return 'Traffic Tinggi, Engagement Rendah'
461
+ else:
462
+ return 'Low Engagement Zone'
463
+
464
+ # Apply archetypes
465
+ summary_table['Arketipe Perilaku'] = summary_table.apply(get_behavioral_archetype, axis=1)
466
+
467
+ # Format table
468
+ summary_table.reset_index(inplace=True)
469
+ summary_table.rename(columns={'shelf_id': 'Rak', 'dwell_time': 'Rata-rata Dwell (s)'}, inplace=True)
470
+ summary_table['Rata-rata Dwell (s)'] = summary_table['Rata-rata Dwell (s)'].round(2)
471
+ summary_table = summary_table.sort_values(by='Interaksi Unik', ascending=False)
472
+
473
+ # Save results
474
+ summary_table.to_csv(os.path.join(output_dir, 'behavioral_archetypes.csv'), index=False)
475
+ return summary_table
476
+ except Exception as e:
477
+ print(f"Error generating behavioral archetypes: {e}")
478
+ return pd.DataFrame({
479
+ 'Rak': ['N/A'],
480
+ 'Interaksi Unik': [0],
481
+ 'Rata-rata Dwell (s)': [0],
482
+ 'Arketipe Perilaku': ['Error']
483
+ })
484
+
485
+ def get_key_metrics(output_dir):
486
+ """Collect key metric visualizations for Gradio interface"""
487
+ heatmap_path = os.path.join(output_dir, 'heatmap.png')
488
+ dwell_time_path = os.path.join(output_dir, 'dwell_time_chart.png')
489
+ journey_path = os.path.join(output_dir, 'journey_chart.png')
490
+
491
+ try:
492
+ archetypes_df = pd.read_csv(os.path.join(output_dir, 'behavioral_archetypes.csv'))
493
+ except:
494
+ archetypes_df = pd.DataFrame({
495
+ 'Rak': ['N/A'],
496
+ 'Interaksi Unik': [0],
497
+ 'Rata-rata Dwell (s)': [0],
498
+ 'Arketipe Perilaku': ['No Data']
499
+ })
500
+
501
+ return heatmap_path, dwell_time_path, journey_path, archetypes_df
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ torch
3
+ numpy
4
+ pandas
5
+ matplotlib
6
+ decord
7
+ ultralytics
8
+ transformers
9
+ supervision
10
+ shapely
11
+ huggingface_hub
12
+ opencv-python
utils/__init__.py ADDED
File without changes
utils/metrics.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import os
4
+ from collections import defaultdict
5
+
6
+ def calculate_dwell_time(action_shelf_log_df, fps):
7
+ """Calculate dwell time per person per shelf"""
8
+ dwell_time_data = []
9
+
10
+ for (pid, shelf_id), group in action_shelf_log_df.groupby(['pid', 'shelf_id']):
11
+ frames = group['frame'].sort_values().tolist()
12
+ if not frames:
13
+ continue
14
+
15
+ segments = []
16
+ segment_start = frames[0]
17
+ prev_frame = frames[0]
18
+
19
+ # Find continuous segments
20
+ for frame in frames[1:]:
21
+ if frame > prev_frame + 3: # Allow small gaps
22
+ segments.append((segment_start, prev_frame))
23
+ segment_start = frame
24
+ prev_frame = frame
25
+ segments.append((segment_start, prev_frame))
26
+
27
+ # Calculate total dwell time across segments
28
+ total_frames = sum(end - start + 1 for start, end in segments)
29
+ dwell_seconds = total_frames / fps
30
+
31
+ dwell_time_data.append({
32
+ 'pid': pid,
33
+ 'shelf_id': shelf_id,
34
+ 'dwell_frames': total_frames,
35
+ 'dwell_time': dwell_seconds
36
+ })
37
+
38
+ return pd.DataFrame(dwell_time_data)
39
+
40
+ def analyze_customer_journey(action_shelf_log_df):
41
+ """Analyze customer journey through shelves"""
42
+ # Extract unique interactions
43
+ interactions = action_shelf_log_df[['pid', 'shelf_id']].drop_duplicates()
44
+
45
+ # Find key events
46
+ reach_events = action_shelf_log_df[action_shelf_log_df['action'] == 'Reach To Shelf'][['pid', 'shelf_id']].drop_duplicates().assign(did_reach=True)
47
+ inspect_events = action_shelf_log_df[action_shelf_log_df['action'] == 'Inspect Product'][['pid', 'shelf_id']].drop_duplicates().assign(did_inspect=True)
48
+ return_events = action_shelf_log_df[action_shelf_log_df['action'] == 'Hand In Shelf'][['pid', 'shelf_id']].drop_duplicates().assign(did_return=True)
49
+
50
+ # Combine into analysis dataframe
51
+ analysis_df = pd.merge(interactions, reach_events, on=['pid', 'shelf_id'], how='left')
52
+ analysis_df = pd.merge(analysis_df, inspect_events, on=['pid', 'shelf_id'], how='left')
53
+ analysis_df = pd.merge(analysis_df, return_events, on=['pid', 'shelf_id'], how='left')
54
+ analysis_df = analysis_df.fillna(False)
55
+
56
+ # Categorize outcomes
57
+ def categorize_outcome(row):
58
+ if not row['did_reach']:
59
+ return 'No Reach' # Ignore interactions without reach
60
+
61
+ if row['did_inspect'] and row['did_return']:
62
+ return 'Keraguan & Pembatalan'
63
+ elif row['did_inspect'] and not row['did_return']:
64
+ return 'Konversi Sukses'
65
+ else:
66
+ return 'Kegagalan Menarik Minat'
67
+
68
+ analysis_df['outcome'] = analysis_df.apply(categorize_outcome, axis=1)
69
+
70
+ return analysis_df
71
+
72
+ def calculate_behavioral_archetypes(dwell_df, journey_df, action_shelf_log_df):
73
+ """Calculate behavioral archetypes for each shelf"""
74
+ # Get unique visitors per shelf
75
+ unique_visitors = action_shelf_log_df.groupby('shelf_id')['pid'].nunique().reset_index()
76
+ unique_visitors.rename(columns={'pid': 'unique_visitors'}, inplace=True)
77
+
78
+ # Get average dwell time
79
+ avg_dwell = dwell_df.groupby('shelf_id')['dwell_time'].mean().reset_index()
80
+
81
+ # Get outcome percentages
82
+ relevant_journey = journey_df[journey_df['outcome'] != 'No Reach']
83
+ outcome_counts = relevant_journey.groupby(['shelf_id', 'outcome']).size().unstack(fill_value=0)
84
+ total_counts = outcome_counts.sum(axis=1)
85
+ outcome_percentage = outcome_counts.div(total_counts, axis=0) * 100
86
+
87
+ # Merge data
88
+ merged_df = pd.merge(unique_visitors, avg_dwell, on='shelf_id', how='outer')
89
+
90
+ # Define archetypes
91
+ archetypes = []
92
+ for _, row in merged_df.iterrows():
93
+ shelf_id = row['shelf_id']
94
+ visitors = row['unique_visitors']
95
+ dwell = row['dwell_time']
96
+
97
+ if shelf_id not in outcome_percentage.index:
98
+ if dwell > 3.0:
99
+ archetype = 'Passive Attention'
100
+ else:
101
+ archetype = 'Low Engagement Zone'
102
+ else:
103
+ outcomes = outcome_percentage.loc[shelf_id]
104
+
105
+ if 'Konversi Sukses' in outcomes and outcomes['Konversi Sukses'] > 20:
106
+ archetype = 'High Conversion'
107
+ elif 'Keraguan & Pembatalan' in outcomes and outcomes['Keraguan & Pembatalan'] > 50:
108
+ archetype = 'High Interest, Low Conversion'
109
+ elif visitors > 10:
110
+ archetype = 'High Traffic, Low Engagement'
111
+ else:
112
+ archetype = 'Low Engagement Zone'
113
+
114
+ archetypes.append({
115
+ 'shelf_id': shelf_id,
116
+ 'unique_visitors': visitors,
117
+ 'avg_dwell_time': dwell,
118
+ 'archetype': archetype
119
+ })
120
+
121
+ return pd.DataFrame(archetypes)
utils/visualization.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import pandas as pd
3
+ import seaborn as sns
4
+ import os
5
+ from collections import defaultdict
6
+ import numpy as np
7
+
8
+ def generate_heatmap_from_grid(heatmap_grid, output_path):
9
+ """Generate a heatmap visualization from a grid of values"""
10
+ plt.figure(figsize=(10, 8))
11
+ sns.heatmap(heatmap_grid, cmap='hot', annot=False)
12
+ plt.title('Customer Traffic Heatmap')
13
+ plt.tight_layout()
14
+ plt.savefig(output_path)
15
+ plt.close()
16
+ return output_path
17
+
18
+ def generate_dwell_time_chart(dwell_df, output_path):
19
+ """Generate a chart showing dwell time per shelf"""
20
+ plt.figure(figsize=(12, 6))
21
+
22
+ # Sort by dwell time descending
23
+ sorted_df = dwell_df.sort_values('dwell_time', ascending=False)
24
+
25
+ # Create bar chart
26
+ plt.bar(sorted_df['shelf_id'], sorted_df['dwell_time'], color='purple')
27
+ plt.title('Average Dwell Time per Shelf', fontsize=14)
28
+ plt.xlabel('Shelf ID')
29
+ plt.ylabel('Time (seconds)')
30
+ plt.xticks(rotation=45)
31
+ plt.tight_layout()
32
+
33
+ plt.savefig(output_path)
34
+ plt.close()
35
+ return output_path
36
+
37
+ def generate_action_distribution_chart(action_summary_df, output_path):
38
+ """Generate a chart showing distribution of customer actions"""
39
+ plt.figure(figsize=(10, 6))
40
+
41
+ # Create pie chart
42
+ action_summary_df.plot.pie(y='count', autopct='%1.1f%%', startangle=90,
43
+ labels=action_summary_df['action'])
44
+ plt.ylabel('')
45
+ plt.title('Distribution of Customer Actions', fontsize=14)
46
+ plt.tight_layout()
47
+
48
+ plt.savefig(output_path)
49
+ plt.close()
50
+ return output_path
51
+
52
+ def generate_journey_chart(journey_df, output_path):
53
+ """Generate a stacked bar chart showing customer journey outcomes per shelf"""
54
+ plt.figure(figsize=(12, 6))
55
+
56
+ # Create stacked bar chart
57
+ journey_df.plot(
58
+ kind='bar',
59
+ stacked=True,
60
+ figsize=(12, 6),
61
+ color=['#2ca02c', '#ff7f0e', '#d62728'] # Green, Orange, Red
62
+ )
63
+ plt.title('Customer Journey Analysis per Shelf', fontsize=14)
64
+ plt.xlabel('Shelf ID')
65
+ plt.ylabel('Percentage')
66
+ plt.legend(title='Journey Outcome')
67
+ plt.xticks(rotation=45)
68
+ plt.tight_layout()
69
+
70
+ plt.savefig(output_path)
71
+ plt.close()
72
+ return output_path
73
+
74
+ def generate_rak_timeline(interaction_csv_path, tracks, shelf_boxes_per_frame, fps, output_path):
75
+ """Generate a timeline visualization of rack interactions"""
76
+ # Load rack interaction data
77
+ rak_df = pd.read_csv(interaction_csv_path)
78
+ valid_raks = set(rak_df['shelf_id'].tolist())
79
+
80
+ # Build timeline per rack
81
+ rak_timeline = defaultdict(list)
82
+ for pid, dets in tracks.items():
83
+ for d in dets:
84
+ f = d['frame']
85
+ x1, y1, x2, y2 = d['bbox']
86
+ px, py = (x1 + x2) / 2, (y1 + y2) / 2
87
+ for sid, (sx1, sy1, sx2, sy2) in shelf_boxes_per_frame.get(f, []):
88
+ if sid not in valid_raks:
89
+ continue
90
+ if sx1 <= px <= sx2 and sy1 <= py <= sy2:
91
+ rak_timeline[sid].append(f)
92
+
93
+ # Create visualization
94
+ plt.figure(figsize=(12, max(4, len(rak_timeline) * 0.4)))
95
+ for i, (rak_id, frames) in enumerate(sorted(rak_timeline.items())):
96
+ if not frames:
97
+ continue
98
+ frames = sorted(frames)
99
+ start = frames[0]
100
+ for j in range(1, len(frames)):
101
+ if frames[j] != frames[j-1] + 1:
102
+ plt.plot([start / fps, frames[j-1] / fps], [i, i], linewidth=6)
103
+ start = frames[j]
104
+ plt.plot([start / fps, frames[-1] / fps], [i, i], linewidth=6)
105
+ plt.text(-1, i, rak_id, verticalalignment='center', fontsize=8)
106
+
107
+ plt.xlabel('Time (seconds)')
108
+ plt.title('Timeline of Shelf Interactions')
109
+ plt.yticks([])
110
+ plt.tight_layout()
111
+ plt.savefig(output_path)
112
+ plt.close()
113
+
114
+ return output_path