A fully reproducible pipeline that synthesises a 224 px cloud-architecture dataset, benchmarks five CNNs under identical conditions, evaluates an R-CNN-style detector against COCO ground truth, and converts detections into a monthly cloud bill.
All five architectures reached 100 % held-out test accuracy and macro-F1 on the 60-image test partition, so the benchmark discriminates on efficiency rather than accuracy: the champion, SimpleCNN (0.43 M params), trains in 67 s on CPU — 332 s faster than the VGG-style model while matching its score. Detection quality is measured, not asserted: the two-stage detector attains mAP@0.5 = 97.5 % over 40 COCO-annotated scenes with per-class P/R/F1 reported, and both FinOps demo scenes reconcile exactly with ground truth ($165.00 and $111.00 monthly bills).
✅ 600 renders written in 2.5s @ 224 px — 3 background styles, full pose/photometric augmentation. ✅ 40 COCO-annotated scenes · 234 instances written in 1.1s -> dataset/scenes/annotations.json 🏁 Campaign finished in 0.0 min 🎯 Detection @ IoU 0.5: mAP@0.5 = 97.5% | evaluated 40 scenes / 234 GT instances in 8.2s 📦 Packaged -> artifacts/cloudsight_dataset.zip (46.2 MB) ✅ Executive report complete.
Cloud cost estimation (FinOps) begins with inventory: knowing what resources a diagram or deployment actually contains. This work builds the smallest honest end-to-end system for that problem — from pixels to dollars — where every intermediate claim is checked against a measurable ground truth.
Contributions. (1) a deterministic, CC0-licensed synthetic dataset of 600 cloud-resource renders across 6 classes at 224 px with full pose/photometric augmentation; (2) 40 COCO-annotated 800×600 detection scenes (234 box instances) with a published annotation file; (3) a fair 5-architecture CNN benchmark with best-epoch checkpointing and multi-seed variance analysis; (4) an R-CNN-style two-stage detector scored with proper VOC-style AP; (5) a transparent FinOps engine that converts detections into monthly cost and reconciles against ground truth.
random) — re-running the
notebook reproduces these numbers bit-for-bit on CPU.The notebook is organised as a linear 13-cell research pipeline:
Each class has a distinct silhouette-first icon (shape carries the signal, colour assists): compute chip, database cylinder, routing hexagon, storage bucket, serverless bolt and gateway gate. Icons are drawn on a 128 px master canvas, then composited at 224 px with randomized rotation (±20°), scale (0.55–1.15), shear (±12°), perspective, three background styles and photometric variation (brightness, blur, noise).
| Property | Value |
|---|---|
| Classification renders | 600 PNG · 224×224 px · 6 × 100 |
| Split (per class) | 70 train / 20 val / 10 test (stratified) |
| Detection scenes | 40 PNG · 800×600 px · 4–8 icons each · 234 GT boxes |
| License | CC0 1.0 — public domain (dataset card included) |
✅ 600 renders written in 2.5s @ 224 px — 3 background styles, full pose/photometric augmentation.
Detection scenes composite 4–8 icons with overlap rejection (intersection must
stay below 25 % of either box) over the same background styles. Every scene is written to
dataset/scenes/annotations.json in COCO format (images,
categories, bbox in xywh), enabling standard tooling and honest evaluation.
Five architectures are trained under identical conditions — same dataloaders, 6 epochs, batch size 8, Adam, StepLR(2, 0.5), best-epoch restoration on validation accuracy. ResNet-18 uses ImageNet-pretrained weights (lr 3e-4); all others train from scratch (lr 1e-3).
| Rank | Model | Test acc (%) | Macro-F1 (%) | Params (M) | Train time (s) |
|---|---|---|---|---|---|
| 1 | SimpleCNN (baseline) CHAMPION | 100.00 | 100.00 | 0.43 | 66.6 |
| 2 | VGG-Style CNN | 100.00 | 100.00 | 0.31 | 331.7 |
| 3 | ResNet-Lite | 100.00 | 100.00 | 1.23 | 119.0 |
| 4 | MobileNet-Lite | 100.00 | 100.00 | 0.03 | 46.7 |
| 5 | ResNet-18 (transfer) | 100.00 | 100.00 | 11.18 | 222.9 |
Total training time 787 s on 2 vCPU. 🏁 Campaign finished in 0.0 min
A single 100 % run can be luck, so the champion is retrained from scratch under seeds 42 / 43 / 44 (short 4-epoch protocol) and variance is reported:
| Seed | Test acc (%) | Macro-F1 (%) |
|---|---|---|
| 42 | 100.00 | 100.00 |
| 43 | 100.00 | 100.00 |
| 44 | 100.00 | 100.00 |
An R-CNN-style two-stage detector: Stage 1 generates region proposals from a multi-cue union of three HSV-saturation thresholds (vivid / mid / faint), morphological cleanup and contour geometry; Stage 2 scores every padded crop with the champion CNN at 224 px. Detections above 0.90 confidence survive class-agnostic NMS (IoU 0.25) and are greedily matched to COCO boxes at IoU ≥ 0.5. AP is computed VOC-style — the precision envelope integrated over recall — so unmatched ground truth correctly caps recall.
| Class | TP | FP | FN | Precision % | Recall % | F1 % | AP@0.5 % |
|---|---|---|---|---|---|---|---|
| Cloud_SQL | 38 | 0 | 0 | 100.0 | 100.0 | 100.0 | 100.0 |
| EC2_Instance | 42 | 0 | 0 | 100.0 | 100.0 | 100.0 | 100.0 |
| Lambda_Function | 35 | 9 | 0 | 79.5 | 100.0 | 88.6 | 97.9 |
| Load_Balancer | 31 | 1 | 1 | 96.9 | 96.9 | 96.9 | 96.9 |
| S3_Bucket | 38 | 6 | 4 | 86.4 | 90.5 | 88.4 | 90.2 |
| VPC_Gateway | 45 | 0 | 0 | 100.0 | 100.0 | 100.0 | 100.0 |
Error analysis. The remaining 5 missed instances are Stage-1 failures — adjacent icons whose saturated regions merge into a single contour, or faint renders whose pixels fall below every saturation cue. False positives (16) arise when background textures produce a confident crop. Both effects are documented honestly rather than tuned away, and they define the ceiling of the classical proposal stage: a learned proposal network is the natural next step (Section 12).
The detector's per-class counts are multiplied by realistic monthly unit prices to produce an instant bill, then reconciled against the COCO ground truth:
| Resource | Represents | Unit price / month |
|---|---|---|
EC2_Instance | Compute chip silhouette — virtual machines | $45.50 |
Cloud_SQL | Database cylinder — managed SQL | $120.00 |
Load_Balancer | Routing hexagon — traffic distribution | $18.00 |
S3_Bucket | Storage bucket — object storage | $12.50 |
Lambda_Function | Serverless bolt — event-driven functions | $35.00 |
VPC_Gateway | Gateway gate — network edge | $28.00 |
| Resource | GT count | ML count | $/unit | GT bill | ML bill | Δ |
|---|---|---|---|---|---|---|
| EC2_Instance | 2 | 2 | 45.50 | $91.00 | $91.00 | |
| Load_Balancer | 1 | 1 | 18.00 | $18.00 | $18.00 | |
| VPC_Gateway | 2 | 2 | 28.00 | $56.00 | $56.00 | |
| Total / month | $165.00 | $165.00 |
| Resource | GT count | ML count | $/unit | GT bill | ML bill | Δ |
|---|---|---|---|---|---|---|
| EC2_Instance | 1 | 1 | 45.50 | $45.50 | $45.50 | |
| S3_Bucket | 3 | 3 | 12.50 | $37.50 | $37.50 | |
| VPC_Gateway | 1 | 1 | 28.00 | $28.00 | $28.00 | |
| Total / month | $111.00 | $111.00 |
CloudSight_AI_FinOps_Estimator.ipynb from the project siteDependencies: torch, torchvision, opencv-python, numpy, pandas, matplotlib, scikit-learn, tqdm — all pre-installed on Colab.
Known limitations. (1) Detection recall is bounded by the classical proposal stage (99.1 % proposal recall); merged adjacent instances cause the 5 misses. (2) All imagery is synthetic — domain gap to real architecture screenshots is expected. (3) Prices are static estimates, not live cloud pricing APIs.
Future work. Learned region proposals (RPN-style) or a YOLO-family single-stage detector; a fine-tuned YOLOv8 baseline would contextualise the classical pipeline's 97.5 % mAP; domain randomisation studies bridging synthetic→real; live AWS price feed integration; a web demo running the champion detector in-browser via ONNX.
CloudSight AI v3.0 demonstrates a complete, honest research arc: a deterministic synthetic dataset with published ground truth, a five-way architecture benchmark whose winner is decided by measured efficiency, variance-quantified robustness, a detection pipeline evaluated with standard VOC-style AP against COCO annotations, and a FinOps layer that turns detections into dollars and reconciles exactly with the truth. Every number in this report is recomputed from scratch on each notebook run — nothing is hardcoded, nothing is simulated.
| Artifact | Description |
|---|---|
dataset/ | 600 renders (train/val/test), 40 scenes, annotations.json, icon masters |
artifacts/training_dashboard.png | four-panel campaign dashboard (Figure 3) |
artifacts/leaderboard.csv | per-model accuracy, F1, params, training time |
artifacts/detection_metrics.csv | per-class TP/FP/FN, P/R/F1, AP@0.5 |
artifacts/seed_robustness.csv | multi-seed variance of the champion |
artifacts/champion_model.pt | SimpleCNN state dict (best epoch) |
artifacts/cloudsight_dataset.zip | 46.2 MB packaged dataset (CC0) |
DATASET_CARD.md | Kaggle-style dataset card: provenance, structure, license |
labels.csv | file → split → class manifest for all 600 images |
Verbatim from the notebook (compact extracts; the full source is the .ipynb itself).
def make_proposals(scene_bgr):
'''Multi-cue union region proposals: three HSV-saturation cues are run
independently and their contour boxes unioned — dense enough to catch both
vivid and faint icons, still cheap enough for CPU inference.'''
hsv = cv2.cvtColor(scene_bgr, cv2.COLOR_BGR2HSV)
H, W = scene_bgr.shape[:2]
props = []
for s_thr, v_thr in ((90, 60), (120, 70), (150, 85)):
mask = ((hsv[..., 1] > s_thr) & (hsv[..., 2] > v_thr)).astype(np.uint8) * 255
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE,
cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN,
cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)))
for c in cv2.findContours(mask, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)[0]:
x, y, w, h = cv2.boundingRect(c)
if w < 20 or h < 20 or w > W * 0.8 or h > H * 0.8:
continue
if not (0.25 < w / h < 4.0):
continue
pad = 10
x0, y0 = max(0, x - pad), max(0, y - pad)
x1, y1 = min(W, x + w + pad), min(H, y + h + pad)
props.append((x0, y0, x1 - x0, y1 - y0))
return props
def detect_scene(model, scene_bgr):
dets = []
for (x, y, w, h) in make_proposals(scene_bgr):
crop = scene_bgr[y:y + h, x:x + w]
cls, conf = classify_crop(model, crop)
if conf >= CFG.det_conf:
dets.append({'label': cls, 'conf': conf, 'box': (x, y, w, h)})
return nms(dets)
BOX_COLORS = dict(zip(CLASS_NAMES,
...
def voc_ap(tp_flags, scores, n_gt):
'''VOC-style AP: integrate the precision envelope over recall.
Unmatched ground truth (FN) correctly caps recall below 1.0.'''
if n_gt == 0 or not len(tp_flags):
return 0.0
order = np.argsort(-np.asarray(scores, dtype=np.float64))
tpf = np.asarray(tp_flags, dtype=np.float64)[order]
ctp, cfp = np.cumsum(tpf), np.cumsum(1.0 - tpf)
recall = ctp / n_gt
prec = ctp / np.maximum(ctp + cfp, 1e-9)
mrec = np.concatenate(([0.0], recall, [recall[-1]]))
mpre = np.concatenate(([0.0], prec, [0.0]))
for i in range(len(mpre) - 2, -1, -1):
mpre[i] = max(mpre[i], mpre[i + 1])
idx = np.where(mrec[1:] != mrec[:-1])[0]
return float(np.sum((mrec[idx + 1] - mrec[idx]) * mpre[idx + 1]))
def train_model(model, name, train_loader, val_loader, epochs=CFG.epochs,
lr=None, verbose=True, ckpt_path=None):
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=lr or MODEL_ZOO[name]['lr'])
sched = optim.lr_scheduler.StepLR(optimizer, step_size=2, gamma=0.5)
start_ep, best_acc, best_state, hist = 0, 0.0, None, []
if ckpt_path and os.path.exists(ckpt_path):
# resume losslessly: model + optim + scheduler + RNG + best-epoch state
ck = torch.load(ckpt_path, map_location='cpu', weights_only=False)
model.load_state_dict(ck['model'])
optimizer.load_state_dict(ck['optim'])
sched.load_state_dict(ck['sched'])
if train_loader.generator is not None and ck.get('gen') is not None:
train_loader.generator.set_state(ck['gen'])
torch.set_rng_state(ck['torch_rng'])
start_ep, best_acc, hist = ck['epoch'], ck['best_acc'], ck['hist']
best_state = ck['best_state']
if verbose:
print(f' [{name[:18]:18s}] ⏩ resumed from epoch {start_ep}')
for ep in range(start_ep + 1, epochs + 1):
t0 = time.time()
tr_loss, tr_acc = run_epoch(model, train_loader, criterion, optimizer)
va_loss, va_acc = run_epoch(model, val_loader, criterion)
sched.step()
hist.append((ep, tr_loss, tr_acc, va_loss, va_acc))
if va_acc >= best_acc:
best_acc = va_acc
best_state = {k: v.detach().cpu().clone()
for k, v in model.state_dict().items()}
if verbose:
print(f' [{name[:18]:18s}] ep {ep}/{epochs} '
f'loss {tr_loss:.3f} acc {tr_acc:5.1f}% | '
f'val acc {va_acc:5.1f}% ({time.time() - t0:4.1f}s)')
if ckpt_path: # epoch-level checkpoint
torch.save({'epoch': ep, 'model': model.state_dict(),
'optim': optimizer.state_dict(),
'sched': sched.state_dict(),
...
def render_ec2():
im = _tile(); p = PAL['EC2_Instance']
cv2.rectangle(im, (30, 30), (97, 97), p['dark'], -1)
cv2.rectangle(im, (34, 34), (93, 93), p['main'], -1)
cv2.rectangle(im, (50, 50), (77, 77), p['lite'], -1)
cv2.rectangle(im, (56, 56), (71, 71), p['dark'], -1)
for i in range(4): # dual pin rows
y = 38 + i * 18
cv2.rectangle(im, (10, y), (28, y + 8), p['main'], -1)
cv2.rectangle(im, (99, y), (117, y + 8), p['main'], -1)
return im