|
|
@@ -0,0 +1,98 @@
|
|
|
+import { Injectable, signal } from '@angular/core';
|
|
|
+import { Observable, from, map, catchError, of, switchMap } from 'rxjs';
|
|
|
+import { LocalInferenceService } from '../../services/local-inference.service';
|
|
|
+import { RemoteInferenceService } from './remote-inference.service';
|
|
|
+import { ImageProcessorService } from '../../services/image-processor.service';
|
|
|
+import { AnalysisResponse, DetectionResult } from '../interfaces/palm-analysis.interface';
|
|
|
+
|
|
|
+export type InferenceMode = 'local' | 'api';
|
|
|
+export type LocalEngine = 'onnx' | 'tflite';
|
|
|
+
|
|
|
+@Injectable({
|
|
|
+ providedIn: 'root'
|
|
|
+})
|
|
|
+export class InferenceService {
|
|
|
+ // Use Signal to track processing mode and local engine
|
|
|
+ public mode = signal<InferenceMode>('local');
|
|
|
+ public localEngine = signal<LocalEngine>('onnx');
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private readonly localInferenceService: LocalInferenceService,
|
|
|
+ private readonly remoteInferenceService: RemoteInferenceService,
|
|
|
+ private readonly imageProcessor: ImageProcessorService
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Main analyze entry point. imageData is base64 string.
|
|
|
+ */
|
|
|
+ analyze(imageData: string, width: number, height: number): Observable<any[]> {
|
|
|
+ if (this.mode() === 'local') {
|
|
|
+ return this.runLocalAnalysis(imageData, width, height);
|
|
|
+ } else {
|
|
|
+ return this.runRemoteAnalysis(imageData);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private runLocalAnalysis(imageData: string, width: number, height: number): Observable<any[]> {
|
|
|
+ const blob = this.base64ToBlob(imageData);
|
|
|
+ const file = new File([blob], 'capture.jpg', { type: 'image/jpeg' });
|
|
|
+
|
|
|
+ const modelPath = this.localEngine() === 'onnx'
|
|
|
+ ? 'assets/models/onnx/best.onnx'
|
|
|
+ : 'assets/models/tflite/best_float32.tflite';
|
|
|
+
|
|
|
+ return from(this.localInferenceService.loadModel(modelPath)).pipe(
|
|
|
+ switchMap(() => from(this.imageProcessor.processImage(file))),
|
|
|
+ switchMap(processedData => from(this.localInferenceService.runInference(processedData))),
|
|
|
+ map(rawData => {
|
|
|
+ if (!rawData) return [];
|
|
|
+ return this.localInferenceService.parseDetections(rawData, 0.25, width, height);
|
|
|
+ }),
|
|
|
+ catchError(err => {
|
|
|
+ console.error('Local Inference Hub Error:', err);
|
|
|
+ return of([]);
|
|
|
+ })
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ private runRemoteAnalysis(imageData: string): Observable<any[]> {
|
|
|
+ const blob = this.base64ToBlob(imageData);
|
|
|
+ return this.remoteInferenceService.analyze(blob).pipe(
|
|
|
+ map((response: AnalysisResponse) => {
|
|
|
+ // Map Result to the internal UI format
|
|
|
+ // Coordinate Sync: Use absolute pixels directly from API
|
|
|
+ return response.detections.map(det => ({
|
|
|
+ box: det.box, // Already [x1, y1, x2, y2]
|
|
|
+ confidence: det.confidence,
|
|
|
+ class: det.class,
|
|
|
+ color: this.getGradeColor(det.class),
|
|
|
+ isHealthAlert: det.is_health_alert
|
|
|
+ }));
|
|
|
+ }),
|
|
|
+ catchError(err => {
|
|
|
+ console.error('Remote Inference Hub Error:', err);
|
|
|
+ return of([]);
|
|
|
+ })
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ private base64ToBlob(base64: string): Blob {
|
|
|
+ const parts = base64.split(',');
|
|
|
+ const byteString = atob(parts[1]);
|
|
|
+ const mimeString = parts[0].split(':')[1].split(';')[0];
|
|
|
+ const ab = new ArrayBuffer(byteString.length);
|
|
|
+ const ia = new Uint8Array(ab);
|
|
|
+ for (let i = 0; i < byteString.length; i++) {
|
|
|
+ ia[i] = byteString.charCodeAt(i);
|
|
|
+ }
|
|
|
+ return new Blob([ab], { type: mimeString });
|
|
|
+ }
|
|
|
+
|
|
|
+ private getGradeColor(className: string): string {
|
|
|
+ const colors: { [key: string]: string } = {
|
|
|
+ 'Empty_Bunch': '#6C757D', 'Underripe': '#F9A825', 'Abnormal': '#DC3545',
|
|
|
+ 'Ripe': '#00A651', 'Unripe': '#9E9D24', 'Overripe': '#5D4037'
|
|
|
+ };
|
|
|
+ return colors[className] || '#000000';
|
|
|
+ }
|
|
|
+}
|