Ver código fonte

overall report enhancements

Dr-Swopt 2 dias atrás
pai
commit
ab7255d989

+ 22 - 0
src/app/components/analyzer/analyzer.component.html

@@ -198,6 +198,21 @@
                   <span class="bc-value">{{ completedReport()!.meta.total_time_ms }} ms</span>
                 </div>
               </div>
+              <!-- Ripeness Distribution -->
+              @if (getBatchDistribution().length > 0) {
+                <div class="bc-distribution">
+                  <div class="bc-dist-label">Ripeness Distribution</div>
+                  @for (item of getBatchDistribution(); track item.class) {
+                    <div class="bc-dist-row">
+                      <span class="bc-dist-name">{{ item.class }}</span>
+                      <div class="bc-dist-bar-track">
+                        <div class="bc-dist-bar-fill" [style.width.%]="item.pct" [style.background]="item.color"></div>
+                      </div>
+                      <span class="bc-dist-pct">{{ item.pct }}%</span>
+                    </div>
+                  }
+                </div>
+              }
               <button class="btn btn-outline btn-sm" (click)="completedReport.set(null)">
                 Dismiss
               </button>
@@ -417,6 +432,13 @@
         <button class="btn btn-outline btn-sm" (click)="selectedAuditEntry.set(null)">← Back to Summary</button>
       </div>
 
+      <!-- Evidence Canvas — image with bounding boxes drawn by drawEvidence() -->
+      @if (selectedAuditEntry()!.localBlobUrl) {
+        <div class="evidence-canvas-wrapper">
+          <canvas #evidenceCanvas class="evidence-canvas"></canvas>
+        </div>
+      }
+
       <div class="evidence-grid">
         <div class="evidence-block">
           <div class="evidence-block-label">Raw Tensor Sample (pre-NMS · 5 rows · [x1, y1, x2, y2, conf, cls])</div>

+ 69 - 0
src/app/components/analyzer/analyzer.component.scss

@@ -802,3 +802,72 @@
   &.ok    { color: var(--accent, #00A651); }
   &.alert { color: #DC3545; }
 }
+
+// ── Batch Complete — Ripeness Distribution ────────────────────────────────────
+.bc-distribution {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  border-top: 1px solid var(--border-color);
+  padding-top: 10px;
+}
+
+.bc-dist-label {
+  font-size: 11px;
+  font-weight: 700;
+  color: var(--text-secondary);
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
+  margin-bottom: 2px;
+}
+
+.bc-dist-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.bc-dist-name {
+  font-size: 11px;
+  color: var(--text-primary);
+  width: 80px;
+  flex-shrink: 0;
+}
+
+.bc-dist-bar-track {
+  flex: 1;
+  height: 8px;
+  background: var(--input-bg, #2a2a2a);
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.bc-dist-bar-fill {
+  height: 100%;
+  border-radius: 4px;
+  transition: width 0.4s ease;
+}
+
+.bc-dist-pct {
+  font-size: 11px;
+  font-variant-numeric: tabular-nums;
+  color: var(--text-secondary);
+  width: 32px;
+  text-align: right;
+  flex-shrink: 0;
+}
+
+// ── Evidence Canvas (batch audit drill-down) ──────────────────────────────────
+.evidence-canvas-wrapper {
+  margin-bottom: 20px;
+  border-radius: 12px;
+  overflow: hidden;
+  border: 1px solid var(--border-color);
+  background: #000;
+}
+
+.evidence-canvas {
+  width: 100% !important;
+  height: auto !important;
+  display: block;
+}

+ 88 - 19
src/app/components/analyzer/analyzer.component.ts

@@ -66,6 +66,8 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
   @ViewChild('resultCanvas') resultCanvasRef!: ElementRef<HTMLCanvasElement>;
   /** Snapshot canvas for socket-engine mode (frozen frame + bounding boxes) */
   @ViewChild('snapCanvas') snapCanvasRef!: ElementRef<HTMLCanvasElement>;
+  /** Evidence canvas for batch audit drill-down (separate from snapCanvas) */
+  @ViewChild('evidenceCanvas') evidenceCanvasRef!: ElementRef<HTMLCanvasElement>;
   /** Live webcam video feed */
   @ViewChild('videoEl') videoElRef!: ElementRef<HTMLVideoElement>;
 
@@ -100,6 +102,8 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
   /** Total files in the current batch run — stored separately so it survives queue drain */
   private _totalBatchCount = 0;
   get totalBatchCount(): number { return this._totalBatchCount; }
+  /** Blob URL for the file currently being processed — stored so the pong effect can attach it to the record */
+  private _currentBlobUrl: string | null = null;
 
   // ── Socket-engine state ────────────────────────────────────────────────────
   /** 'webcam' = live camera snap | 'gallery' = file from device storage */
@@ -169,7 +173,9 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
           industrial_summary: res.industrial_summary,
           raw_tensor_sample: res.technical_evidence?.raw_tensor_sample ?? res.raw_tensor_sample ?? [],
         },
+        localBlobUrl: this._currentBlobUrl ?? undefined,
       };
+      this._currentBlobUrl = null;
       this.sessionManifest.push(record);
 
       const next = idx + 1;
@@ -219,6 +225,14 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
         this.finalizeBatch();
       }
     });
+
+    // Draw evidence canvas whenever the user selects an audit entry
+    effect(() => {
+      const entry = this.selectedAuditEntry();
+      if (!entry) return;
+      // defer one tick so #evidenceCanvas is rendered in the DOM
+      setTimeout(() => this.drawEvidence(entry), 0);
+    });
   }
 
   ngOnInit(): void {}
@@ -277,29 +291,25 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
     const file = this.batchQueue()[this.currentBatchIndex()];
     if (!file) return;
 
-    // Render file → 640×640 offscreen canvas → JPEG Base64 (Lego 11 format)
-    const reader = new FileReader();
-    reader.onload = (e) => {
-      const src = e.target?.result as string;
-      if (!src) { this.advanceBatchOnError(file.name, 'FileReader returned empty result'); return; }
+    // Create a blob URL for the evidence canvas (revoked in finalizeBatch/abortBatch)
+    const blobUrl = URL.createObjectURL(file);
+    this._currentBlobUrl = blobUrl;
 
-      const img = new Image();
-      img.onerror = () => this.advanceBatchOnError(file.name, 'Image failed to decode');
-      img.onload = () => {
-        const offscreen = document.createElement('canvas');
-        offscreen.width = 640;
-        offscreen.height = 640;
-        offscreen.getContext('2d')!.drawImage(img, 0, 0, 640, 640);
-        const base64 = offscreen.toDataURL('image/jpeg');
-        this._pingTime = performance.now();
-        this.visionSocket.sendBase64(base64);
-      };
-      img.src = src;
+    const img = new Image();
+    img.onerror = () => { URL.revokeObjectURL(blobUrl); this.advanceBatchOnError(file.name, 'Image failed to decode'); };
+    img.onload = () => {
+      const offscreen = document.createElement('canvas');
+      offscreen.width = 640;
+      offscreen.height = 640;
+      offscreen.getContext('2d')!.drawImage(img, 0, 0, 640, 640);
+      const base64 = offscreen.toDataURL('image/jpeg');
+      this._pingTime = performance.now();
+      this.visionSocket.sendBase64(base64);
     };
-    reader.onerror = () => this.advanceBatchOnError(file.name, 'FileReader error');
-    reader.readAsDataURL(file);
+    img.src = blobUrl;
   }
 
+
   /** Log a client-side read/decode error and advance to the next file */
   private advanceBatchOnError(fileName: string, reason: string): void {
     const record: BatchResult = {
@@ -359,11 +369,49 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
   }
 
   abortBatch(): void {
+    // Revoke any pending blob URL for the in-flight image
+    if (this._currentBlobUrl) { URL.revokeObjectURL(this._currentBlobUrl); this._currentBlobUrl = null; }
+    // Revoke all stored evidence URLs
+    this.sessionManifest.forEach(r => { if (r.localBlobUrl) URL.revokeObjectURL(r.localBlobUrl); });
     this.batchQueue.set([]);
     this.currentBatchIndex.set(0);
     this._batchRunning.set(false);
   }
 
+  /** Draw the source image + bounding boxes onto the dedicated evidence canvas */
+  private drawEvidence(entry: BatchResult): void {
+    const canvas = this.evidenceCanvasRef?.nativeElement;
+    if (!canvas || !entry.localBlobUrl) return;
+
+    const img = new Image();
+    img.onload = () => {
+      const size = canvas.parentElement!.clientWidth || 640;
+      canvas.width = size;
+      canvas.height = size;
+      const ctx = canvas.getContext('2d')!;
+      ctx.drawImage(img, 0, 0, size, size);
+
+      const scaleX = size / 640;
+      const scaleY = size / 640;
+
+      entry.detections.forEach(det => {
+        const { x1, y1, x2, y2 } = det.bounding_box;
+        const color = GRADE_COLORS[det.ripeness_class] || '#00A651';
+        ctx.strokeStyle = color;
+        ctx.lineWidth = 3;
+        ctx.strokeRect(x1 * scaleX, y1 * scaleY, (x2 - x1) * scaleX, (y2 - y1) * scaleY);
+        ctx.fillStyle = color;
+        ctx.font = 'bold 13px Outfit';
+        const label = `${det.ripeness_class} ${det.confidence_pct.toFixed(1)}%`;
+        const tw = ctx.measureText(label).width;
+        ctx.fillRect(x1 * scaleX, y1 * scaleY - 22, tw + 8, 22);
+        ctx.fillStyle = '#fff';
+        ctx.fillText(label, x1 * scaleX + 4, y1 * scaleY - 6);
+      });
+    };
+    img.src = entry.localBlobUrl;
+  }
+
   onDragOver(event: DragEvent): void {
     event.preventDefault();
     event.stopPropagation();
@@ -690,4 +738,25 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
   getSummaryKeys(): string[] {
     return Object.keys(this.inferenceService.summary() ?? {});
   }
+
+  /** Aggregate industrial_summary across all successful batch results */
+  getBatchDistribution(): { class: string; count: number; pct: number; color: string }[] {
+    const report = this.completedReport();
+    if (!report) return [];
+    const totals: Record<string, number> = {};
+    report.results.filter(r => r.status === 'ok').forEach(r => {
+      Object.entries(r.technical_evidence.industrial_summary).forEach(([cls, n]) => {
+        totals[cls] = (totals[cls] ?? 0) + n;
+      });
+    });
+    const grand = Object.values(totals).reduce((s, n) => s + n, 0) || 1;
+    return Object.entries(totals)
+      .sort((a, b) => b[1] - a[1])
+      .map(([cls, count]) => ({
+        class: cls,
+        count,
+        pct: Math.round((count / grand) * 100),
+        color: GRADE_COLORS[cls] ?? '#888',
+      }));
+  }
 }

+ 1 - 0
src/app/core/interfaces/palm-analysis.interface.ts

@@ -27,6 +27,7 @@ export interface BatchResult {
     industrial_summary: Record<string, number>;
     raw_tensor_sample: number[][];
   };
+  localBlobUrl?: string;       // temporary object URL for evidence canvas; revoke after use
   error?: string;
 }