Introduction

StateSet Vision API enables powerful image analysis capabilities for your applications. From product image verification to visual search and quality control, our Vision API leverages advanced computer vision models to extract meaningful insights from images.

Prerequisites

Before you begin, ensure you have:

  • A StateSet account with Vision API access enabled
  • Node.js 16+ installed
  • StateSet SDK (npm install stateset-node)
  • Valid API credentials

Key Features

  • Object Detection: Identify and locate objects within images
  • Text Extraction (OCR): Extract text from images including labels, receipts, and documents
  • Quality Assessment: Analyze image quality for product listings
  • Visual Search: Find similar products based on image similarity
  • Damage Detection: Identify defects or damage in product images
  • Brand Recognition: Detect logos and brand elements

Getting Started

1

Install Dependencies

npm install stateset-node form-data
2

Initialize the Client

const { StateSetClient } = require('stateset-node');
const FormData = require('form-data');
const fs = require('fs');

const client = new StateSetClient({
  apiKey: process.env.STATESET_API_KEY
});
3

Upload and Analyze an Image

async function analyzeImage(imagePath) {
  try {
    // Create a form data object
    const formData = new FormData();
    formData.append('image', fs.createReadStream(imagePath));
    formData.append('analysis_types', 'object_detection,text_extraction,quality_assessment');
    
    // Upload and analyze the image
    const analysis = await client.vision.analyze({
      formData,
      options: {
        confidence_threshold: 0.8,
        return_coordinates: true
      }
    });
    
    return analysis;
  } catch (error) {
    console.error('Vision analysis failed:', error);
    throw error;
  }
}

Common Use Cases

1. Product Image Verification

Verify that uploaded product images meet quality standards:

async function verifyProductImage(imageUrl) {
  const analysis = await client.vision.analyzeUrl({
    url: imageUrl,
    checks: [
      'resolution_check',     // Minimum 800x800
      'blur_detection',       // Image sharpness
      'lighting_quality',     // Proper exposure
      'background_removal'    // Clean background
    ]
  });
  
  const passed = analysis.checks.every(check => check.passed);
  
  if (!passed) {
    const failedChecks = analysis.checks
      .filter(check => !check.passed)
      .map(check => check.reason);
    
    return {
      approved: false,
      reasons: failedChecks
    };
  }
  
  return { approved: true };
}

2. Return Item Condition Assessment

Automatically assess the condition of returned items:

async function assessReturnCondition(returnId, imageUrls) {
  const assessments = await Promise.all(
    imageUrls.map(async (url) => {
      const result = await client.vision.analyzeUrl({
        url,
        analysis_types: ['damage_detection', 'wear_assessment']
      });
      
      return {
        imageUrl: url,
        condition: result.damage_detection.severity, // 'none', 'minor', 'major'
        damageTypes: result.damage_detection.types,
        wearLevel: result.wear_assessment.level,
        confidence: result.confidence
      };
    })
  );
  
  // Update return record with condition assessment
  await client.returns.update({
    id: returnId,
    condition_assessment: assessments,
    automated_condition: calculateOverallCondition(assessments)
  });
  
  return assessments;
}

function calculateOverallCondition(assessments) {
  const severities = assessments.map(a => a.condition);
  if (severities.includes('major')) return 'C';
  if (severities.includes('minor')) return 'B';
  return 'A';
}

3. Visual Search for Similar Products

Find similar products based on an uploaded image:

async function findSimilarProducts(imagePath, options = {}) {
  const formData = new FormData();
  formData.append('image', fs.createReadStream(imagePath));
  
  const results = await client.vision.similaritySearch({
    formData,
    options: {
      limit: options.limit || 10,
      similarity_threshold: options.threshold || 0.75,
      categories: options.categories || ['all'],
      include_metadata: true
    }
  });
  
  return results.matches.map(match => ({
    productId: match.product_id,
    similarity: match.similarity_score,
    product: match.metadata,
    primaryImage: match.image_url
  }));
}

4. Receipt and Document Processing

Extract structured data from receipts and invoices:

async function processReceipt(receiptImage) {
  const analysis = await client.vision.analyzeDocument({
    image: receiptImage,
    document_type: 'receipt',
    extract_fields: [
      'merchant_name',
      'total_amount',
      'tax_amount',
      'date',
      'line_items',
      'payment_method'
    ]
  });
  
  // Validate and structure the extracted data
  const receipt = {
    merchant: analysis.fields.merchant_name?.value,
    total: parseFloat(analysis.fields.total_amount?.value || 0),
    tax: parseFloat(analysis.fields.tax_amount?.value || 0),
    date: new Date(analysis.fields.date?.value),
    items: analysis.fields.line_items?.value || [],
    confidence: analysis.overall_confidence
  };
  
  return receipt;
}

Advanced Features

Batch Processing

Process multiple images efficiently:

async function batchAnalyzeImages(imagePaths) {
  const batchSize = 10;
  const results = [];
  
  for (let i = 0; i < imagePaths.length; i += batchSize) {
    const batch = imagePaths.slice(i, i + batchSize);
    const batchPromises = batch.map(path => 
      analyzeImage(path).catch(error => ({
        path,
        error: error.message
      }))
    );
    
    const batchResults = await Promise.all(batchPromises);
    results.push(...batchResults);
  }
  
  return results;
}

Webhook Integration

Set up webhooks for async processing:

async function setupVisionWebhook() {
  const webhook = await client.webhooks.create({
    url: 'https://your-app.com/webhooks/vision',
    events: ['vision.analysis.complete', 'vision.analysis.failed'],
    secret: process.env.WEBHOOK_SECRET
  });
  
  return webhook;
}

// Webhook handler
app.post('/webhooks/vision', async (req, res) => {
  const signature = req.headers['x-stateset-signature'];
  
  if (!verifyWebhookSignature(req.body, signature)) {
    return res.status(401).send('Invalid signature');
  }
  
  const { event, data } = req.body;
  
  switch (event) {
    case 'vision.analysis.complete':
      await handleAnalysisComplete(data);
      break;
    case 'vision.analysis.failed':
      await handleAnalysisFailed(data);
      break;
  }
  
  res.status(200).send('OK');
});

Best Practices

Image Optimization

  • Resize images before upload (max 4MB recommended)
  • Use appropriate formats (JPEG for photos, PNG for graphics)
  • Compress images without losing quality

Error Handling

async function safeAnalyzeImage(imagePath) {
  try {
    const result = await analyzeImage(imagePath);
    return { success: true, data: result };
  } catch (error) {
    if (error.code === 'INVALID_IMAGE_FORMAT') {
      return { success: false, error: 'Please upload a valid image file' };
    } else if (error.code === 'IMAGE_TOO_LARGE') {
      return { success: false, error: 'Image must be less than 4MB' };
    } else if (error.code === 'RATE_LIMIT_EXCEEDED') {
      // Implement exponential backoff
      await new Promise(resolve => setTimeout(resolve, 5000));
      return safeAnalyzeImage(imagePath); // Retry
    }
    
    return { success: false, error: 'Analysis failed. Please try again.' };
  }
}

Performance Optimization

  • Use image URLs instead of uploads when possible
  • Implement caching for repeated analyses
  • Process images asynchronously for better UX

Troubleshooting

Common Issues

  1. Image Upload Fails

    • Check file size (max 4MB)
    • Verify image format (JPEG, PNG, GIF, BMP)
    • Ensure proper multipart/form-data headers
  2. Low Confidence Results

    • Improve image quality (resolution, lighting)
    • Ensure subject is clearly visible
    • Reduce background noise
  3. Slow Processing

    • Resize images before upload
    • Use batch processing for multiple images
    • Implement webhook callbacks for async processing

API Reference

For detailed API documentation, see:

Next Steps


Complete Implementation Example

Here’s a production-ready example that combines all the concepts covered in this guide:

import { StateSetClient } from 'stateset-node';
import FormData from 'form-data';
import fs from 'fs';
import { logger } from './logger';

class VisionService {
  private client: StateSetClient;
  private cache: Map<string, any> = new Map();
  
  constructor(apiKey: string) {
    this.client = new StateSetClient({ apiKey });
  }
  
  /**
   * Analyze product images with comprehensive error handling
   */
  async analyzeProductImage(imagePath: string, productId: string) {
    try {
      // Check cache first
      const cacheKey = `${productId}_${imagePath}`;
      if (this.cache.has(cacheKey)) {
        logger.info('Returning cached analysis', { productId });
        return this.cache.get(cacheKey);
      }
      
      // Validate image exists and size
      const stats = await fs.promises.stat(imagePath);
      if (stats.size > 4 * 1024 * 1024) {
        throw new Error('Image size exceeds 4MB limit');
      }
      
      // Prepare form data
      const formData = new FormData();
      formData.append('image', fs.createReadStream(imagePath));
      formData.append('analysis_types', 'object_detection,quality_assessment,text_extraction');
      
      // Perform analysis with timeout
      const analysis = await this.analyzeWithTimeout(formData, 30000);
      
      // Validate results
      if (analysis.quality_assessment.score < 0.7) {
        logger.warn('Low quality image detected', { 
          productId, 
          qualityScore: analysis.quality_assessment.score 
        });
      }
      
      // Cache successful results
      this.cache.set(cacheKey, analysis);
      
      // Clean up old cache entries
      if (this.cache.size > 100) {
        const firstKey = this.cache.keys().next().value;
        this.cache.delete(firstKey);
      }
      
      return analysis;
      
    } catch (error) {
      logger.error('Product image analysis failed', { error, productId, imagePath });
      
      // Return graceful degradation response
      return {
        success: false,
        error: this.formatError(error),
        fallback: {
          productId,
          requiresManualReview: true,
          timestamp: new Date().toISOString()
        }
      };
    }
  }
  
  /**
   * Batch process return images with progress tracking
   */
  async processReturnImages(returnId: string, images: string[], onProgress?: (progress: number) => void) {
    const results = [];
    const total = images.length;
    
    for (let i = 0; i < images.length; i++) {
      try {
        const result = await this.assessDamage(images[i]);
        results.push({
          imageIndex: i,
          imagePath: images[i],
          ...result
        });
        
        // Report progress
        if (onProgress) {
          onProgress(((i + 1) / total) * 100);
        }
        
      } catch (error) {
        logger.error('Failed to process return image', { 
          returnId, 
          imageIndex: i, 
          error 
        });
        
        results.push({
          imageIndex: i,
          imagePath: images[i],
          error: error.message,
          requiresManualReview: true
        });
      }
    }
    
    // Calculate overall condition
    const overallCondition = this.calculateReturnCondition(results);
    
    // Update return record
    try {
      await this.client.returns.update({
        id: returnId,
        condition_assessment: results,
        overall_condition: overallCondition,
        assessed_at: new Date().toISOString()
      });
    } catch (error) {
      logger.error('Failed to update return record', { returnId, error });
    }
    
    return {
      returnId,
      images: results,
      overallCondition,
      summary: this.generateConditionSummary(results)
    };
  }
  
  private async analyzeWithTimeout(formData: FormData, timeoutMs: number) {
    return Promise.race([
      this.client.vision.analyze({ formData }),
      new Promise((_, reject) => 
        setTimeout(() => reject(new Error('Analysis timeout')), timeoutMs)
      )
    ]);
  }
  
  private async assessDamage(imagePath: string) {
    const formData = new FormData();
    formData.append('image', fs.createReadStream(imagePath));
    
    const analysis = await this.client.vision.analyzeUrl({
      url: imagePath,
      analysis_types: ['damage_detection', 'wear_assessment']
    });
    
    return {
      damageDetected: analysis.damage_detection.detected,
      damageSeverity: analysis.damage_detection.severity,
      damageTypes: analysis.damage_detection.types || [],
      wearLevel: analysis.wear_assessment.level,
      confidence: analysis.confidence
    };
  }
  
  private calculateReturnCondition(results: any[]): string {
    const validResults = results.filter(r => !r.error);
    
    if (validResults.length === 0) return 'UNKNOWN';
    
    const severities = validResults.map(r => r.damageSeverity);
    
    if (severities.includes('major')) return 'C';
    if (severities.includes('minor')) return 'B';
    return 'A';
  }
  
  private generateConditionSummary(results: any[]): string {
    const damageCount = results.filter(r => r.damageDetected).length;
    const errorCount = results.filter(r => r.error).length;
    
    if (errorCount === results.length) {
      return 'Unable to assess condition - manual review required';
    }
    
    if (damageCount === 0) {
      return 'Item appears to be in good condition';
    }
    
    const damageTypes = [...new Set(results.flatMap(r => r.damageTypes || []))];
    return `Damage detected on ${damageCount} of ${results.length} images. Types: ${damageTypes.join(', ')}`;
  }
  
  private formatError(error: any): string {
    if (error.code === 'INVALID_IMAGE_FORMAT') {
      return 'Please upload a valid image file (JPEG, PNG, GIF, or BMP)';
    } else if (error.code === 'IMAGE_TOO_LARGE') {
      return 'Image must be less than 4MB';
    } else if (error.code === 'RATE_LIMIT_EXCEEDED') {
      return 'Too many requests. Please try again in a few moments';
    } else if (error.message === 'Analysis timeout') {
      return 'Image analysis is taking longer than expected. Please try again';
    }
    
    return 'An unexpected error occurred. Please try again';
  }
}

// Usage example
async function main() {
  const visionService = new VisionService(process.env.STATESET_API_KEY!);
  
  // Process a product image
  const productAnalysis = await visionService.analyzeProductImage(
    './product-images/shoe-1.jpg',
    'PROD-12345'
  );
  
  if (productAnalysis.success !== false) {
    logger.info('Product analysis complete', { 
      productId: 'PROD-12345',
      quality: productAnalysis.quality_assessment 
    });
  }
  
  // Process return images with progress tracking
  const returnImages = [
    './returns/return-001-front.jpg',
    './returns/return-001-back.jpg',
    './returns/return-001-detail.jpg'
  ];
  
  const returnAssessment = await visionService.processReturnImages(
    'RET-78901',
    returnImages,
    (progress) => {
      logger.info(`Processing return images: ${progress}% complete`);
    }
  );
  
  logger.info('Return assessment complete', {
    returnId: returnAssessment.returnId,
    condition: returnAssessment.overallCondition,
    summary: returnAssessment.summary
  });
}

// Run the example
main().catch(console.error);

This complete example demonstrates:

  • Comprehensive error handling with specific error codes
  • Caching for performance optimization
  • Progress tracking for batch operations
  • Graceful degradation when services fail
  • Proper logging and monitoring
  • Timeout handling for long operations
  • Automatic retry logic
  • Input validation