Back to Blog
7 min read

Azure Custom Vision Updates: Enhanced Model Training and Deployment

Introduction

Azure Custom Vision continues to evolve with significant updates that make it easier to build, train, and deploy custom image classification and object detection models. This post covers the latest features and best practices for leveraging Custom Vision in your applications.

Latest Custom Vision Features

AutoML-Powered Training

import os
from azure.cognitiveservices.vision.customvision.training import CustomVisionTrainingClient
from azure.cognitiveservices.vision.customvision.training.models import (
    Domain,
    Project,
    ImageFileCreateBatch,
    ImageFileCreateEntry,
    Region
)
from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient
from msrest.authentication import ApiKeyCredentials

class CustomVisionTrainer:
    def __init__(self):
        credentials = ApiKeyCredentials(
            in_headers={"Training-key": os.getenv("CUSTOM_VISION_TRAINING_KEY")}
        )
        self.trainer = CustomVisionTrainingClient(
            os.getenv("CUSTOM_VISION_ENDPOINT"),
            credentials
        )

        pred_credentials = ApiKeyCredentials(
            in_headers={"Prediction-key": os.getenv("CUSTOM_VISION_PREDICTION_KEY")}
        )
        self.predictor = CustomVisionPredictionClient(
            os.getenv("CUSTOM_VISION_ENDPOINT"),
            pred_credentials
        )

    def list_domains(self) -> list:
        """List available training domains"""
        domains = self.trainer.get_domains()
        return [
            {
                "id": d.id,
                "name": d.name,
                "type": d.type,
                "exportable": d.exportable
            }
            for d in domains
        ]

    def create_classification_project(
        self,
        name: str,
        classification_type: str = "Multiclass",
        exportable: bool = True
    ) -> str:
        """Create image classification project"""
        domains = self.trainer.get_domains()

        # Find appropriate domain
        if exportable:
            domain = next(
                (d for d in domains
                 if d.type == "Classification"
                 and d.exportable
                 and "General" in d.name),
                None
            )
        else:
            domain = next(
                (d for d in domains
                 if d.type == "Classification"
                 and "General" in d.name),
                None
            )

        project = self.trainer.create_project(
            name,
            domain_id=domain.id,
            classification_type=classification_type
        )

        return project.id

    def create_detection_project(
        self,
        name: str,
        exportable: bool = True
    ) -> str:
        """Create object detection project"""
        domains = self.trainer.get_domains()

        if exportable:
            domain = next(
                (d for d in domains
                 if d.type == "ObjectDetection" and d.exportable),
                None
            )
        else:
            domain = next(
                (d for d in domains if d.type == "ObjectDetection"),
                None
            )

        project = self.trainer.create_project(
            name,
            domain_id=domain.id
        )

        return project.id

# Usage
trainer = CustomVisionTrainer()

# List available domains
domains = trainer.list_domains()
for d in domains:
    print(f"{d['name']} - Type: {d['type']}, Exportable: {d['exportable']}")

# Create project
project_id = trainer.create_classification_project(
    "Product Quality Inspector",
    classification_type="Multiclass",
    exportable=True
)

Enhanced Training Pipeline

import time
from typing import List, Dict, Optional
from dataclasses import dataclass

@dataclass
class TrainingImage:
    url: str
    tags: List[str]
    regions: Optional[List[Dict]] = None  # For object detection

class EnhancedTrainingPipeline:
    def __init__(self, trainer: CustomVisionTrainer):
        self.trainer = trainer
        self.tag_cache = {}

    def prepare_tags(self, project_id: str, tag_names: List[str]) -> Dict[str, str]:
        """Create or get tags for project"""
        existing_tags = self.trainer.trainer.get_tags(project_id)
        tag_map = {t.name: t.id for t in existing_tags}

        for name in tag_names:
            if name not in tag_map:
                tag = self.trainer.trainer.create_tag(project_id, name)
                tag_map[name] = tag.id

        return tag_map

    def upload_classification_images(
        self,
        project_id: str,
        images: List[TrainingImage],
        batch_size: int = 64
    ) -> dict:
        """Upload images for classification training"""
        # Collect all unique tags
        all_tags = set()
        for img in images:
            all_tags.update(img.tags)

        # Prepare tags
        tag_map = self.prepare_tags(project_id, list(all_tags))

        # Create image entries
        entries = []
        for img in images:
            tag_ids = [tag_map[t] for t in img.tags]
            entries.append(ImageFileCreateEntry(url=img.url, tag_ids=tag_ids))

        # Upload in batches
        results = {"success": 0, "failed": 0, "duplicates": 0}

        for i in range(0, len(entries), batch_size):
            batch = entries[i:i+batch_size]
            result = self.trainer.trainer.create_images_from_urls(
                project_id,
                ImageFileCreateBatch(images=batch)
            )

            for img_result in result.images:
                if img_result.status == "OK":
                    results["success"] += 1
                elif img_result.status == "OKDuplicate":
                    results["duplicates"] += 1
                else:
                    results["failed"] += 1

        return results

    def upload_detection_images(
        self,
        project_id: str,
        images: List[TrainingImage],
        batch_size: int = 64
    ) -> dict:
        """Upload images for object detection training"""
        # Collect all unique tags
        all_tags = set()
        for img in images:
            all_tags.update(img.tags)

        # Prepare tags
        tag_map = self.prepare_tags(project_id, list(all_tags))

        # Create image entries with regions
        entries = []
        for img in images:
            regions = []
            if img.regions:
                for region in img.regions:
                    regions.append(Region(
                        tag_id=tag_map[region["tag"]],
                        left=region["left"],
                        top=region["top"],
                        width=region["width"],
                        height=region["height"]
                    ))

            entries.append(ImageFileCreateEntry(
                url=img.url,
                regions=regions
            ))

        # Upload in batches
        results = {"success": 0, "failed": 0, "duplicates": 0}

        for i in range(0, len(entries), batch_size):
            batch = entries[i:i+batch_size]
            result = self.trainer.trainer.create_images_from_urls(
                project_id,
                ImageFileCreateBatch(images=batch)
            )

            for img_result in result.images:
                if img_result.status == "OK":
                    results["success"] += 1
                elif img_result.status == "OKDuplicate":
                    results["duplicates"] += 1
                else:
                    results["failed"] += 1

        return results

    def train_with_validation(
        self,
        project_id: str,
        reserved_budget_hours: int = 1
    ) -> dict:
        """Train model with automatic validation"""
        # Start training
        iteration = self.trainer.trainer.train_project(
            project_id,
            reserved_budget_in_hours=reserved_budget_hours,
            force_train=False
        )

        # Wait for completion
        while iteration.status in ["Queued", "Training"]:
            print(f"Training status: {iteration.status}")
            time.sleep(30)
            iteration = self.trainer.trainer.get_iteration(project_id, iteration.id)

        # Get performance metrics
        performance = self.trainer.trainer.get_iteration_performance(
            project_id,
            iteration.id
        )

        return {
            "iteration_id": iteration.id,
            "status": iteration.status,
            "precision": performance.precision,
            "recall": performance.recall,
            "average_precision": performance.average_precision,
            "per_tag_performance": [
                {
                    "tag": tp.name,
                    "precision": tp.precision,
                    "recall": tp.recall,
                    "average_precision": tp.average_precision
                }
                for tp in performance.per_tag_performance
            ]
        }

# Usage
pipeline = EnhancedTrainingPipeline(trainer)

# Upload classification images
classification_images = [
    TrainingImage(url="https://example.com/good1.jpg", tags=["good_quality"]),
    TrainingImage(url="https://example.com/bad1.jpg", tags=["defective"]),
    # ... more images
]

upload_result = pipeline.upload_classification_images(project_id, classification_images)
print(f"Uploaded: {upload_result}")

# Train model
training_result = pipeline.train_with_validation(project_id)
print(f"Training complete - Precision: {training_result['precision']:.2%}")

Edge Deployment Options

Exporting Models for Edge Devices

class EdgeDeployment:
    def __init__(self, trainer: CustomVisionTrainer):
        self.trainer = trainer

    def export_model(
        self,
        project_id: str,
        iteration_id: str,
        platform: str,
        flavor: str = None
    ) -> str:
        """Export model for edge deployment

        Platforms: "CoreML", "TensorFlow", "DockerFile", "ONNX", "VAIDK"
        Flavors depend on platform (e.g., "TensorFlowLite" for TensorFlow)
        """
        exports = self.trainer.trainer.get_exports(project_id, iteration_id)

        # Check if export already exists
        existing = next(
            (e for e in exports if e.platform == platform and e.flavor == flavor),
            None
        )

        if existing and existing.status == "Done":
            return existing.download_uri

        # Create new export
        export = self.trainer.trainer.export_iteration(
            project_id,
            iteration_id,
            platform,
            flavor=flavor
        )

        # Wait for export
        while export.status in ["Exporting"]:
            time.sleep(5)
            exports = self.trainer.trainer.get_exports(project_id, iteration_id)
            export = next(e for e in exports if e.platform == platform)

        if export.status == "Done":
            return export.download_uri
        else:
            raise Exception(f"Export failed: {export.status}")

    def get_docker_image(
        self,
        project_id: str,
        iteration_id: str,
        platform: str = "Linux"
    ) -> str:
        """Get Docker image for containerized deployment"""
        # Export as DockerFile
        if platform == "Linux":
            flavor = "Linux"
        elif platform == "Windows":
            flavor = "Windows"
        elif platform == "ARM":
            flavor = "ARM"
        else:
            flavor = "Linux"

        return self.export_model(
            project_id,
            iteration_id,
            "DockerFile",
            flavor
        )

    def export_to_onnx(self, project_id: str, iteration_id: str) -> str:
        """Export model to ONNX format"""
        return self.export_model(project_id, iteration_id, "ONNX")

    def export_to_tensorflow_lite(self, project_id: str, iteration_id: str) -> str:
        """Export model to TensorFlow Lite for mobile"""
        return self.export_model(
            project_id,
            iteration_id,
            "TensorFlow",
            "TensorFlowLite"
        )

    def export_to_coreml(self, project_id: str, iteration_id: str) -> str:
        """Export model to CoreML for iOS"""
        return self.export_model(project_id, iteration_id, "CoreML")

# Usage
edge = EdgeDeployment(trainer)

# Export to different formats
onnx_url = edge.export_to_onnx(project_id, iteration_id)
print(f"ONNX model: {onnx_url}")

tflite_url = edge.export_to_tensorflow_lite(project_id, iteration_id)
print(f"TFLite model: {tflite_url}")

docker_url = edge.get_docker_image(project_id, iteration_id, "Linux")
print(f"Docker image: {docker_url}")

Running Exported Models Locally

import onnxruntime as ort
import numpy as np
from PIL import Image

class ONNXModelRunner:
    def __init__(self, model_path: str, labels_path: str):
        self.session = ort.InferenceSession(model_path)
        self.input_name = self.session.get_inputs()[0].name

        with open(labels_path, "r") as f:
            self.labels = [line.strip() for line in f.readlines()]

    def preprocess_image(self, image_path: str, target_size: tuple = (224, 224)) -> np.ndarray:
        """Preprocess image for inference"""
        img = Image.open(image_path).convert("RGB")
        img = img.resize(target_size)

        # Convert to numpy and normalize
        img_array = np.array(img).astype(np.float32)
        img_array = img_array / 255.0

        # Add batch dimension and transpose to NCHW
        img_array = np.transpose(img_array, (2, 0, 1))
        img_array = np.expand_dims(img_array, axis=0)

        return img_array

    def predict(self, image_path: str) -> list:
        """Run prediction on image"""
        input_data = self.preprocess_image(image_path)

        outputs = self.session.run(None, {self.input_name: input_data})
        probabilities = outputs[0][0]

        results = []
        for i, prob in enumerate(probabilities):
            results.append({
                "label": self.labels[i],
                "probability": float(prob)
            })

        results.sort(key=lambda x: x["probability"], reverse=True)
        return results

# Usage
model = ONNXModelRunner("model.onnx", "labels.txt")
predictions = model.predict("test_image.jpg")

for pred in predictions[:3]:
    print(f"{pred['label']}: {pred['probability']:.2%}")

Conclusion

Azure Custom Vision’s latest updates provide powerful capabilities for building custom image classification and object detection models. With AutoML-powered training, enhanced performance metrics, and flexible edge deployment options, it’s easier than ever to create production-ready computer vision solutions. Whether deploying to the cloud or edge devices, Custom Vision offers a complete workflow from training to deployment.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.