Custom Vision Models
Azure Custom Vision lets you train your own computer vision model using your own photographs — without writing machine learning code. You upload labelled images, click Train, and Azure builds a model that can classify images or detect objects with bounding boxes. This topic covers the complete workflow from labelling to publishing to evaluation metrics.
Custom Vision terms — defined before code
Answering the question: "What is in this image overall?" — assigning one or more category labels to the whole image. Example: a photo of a cat → label: "Cat". A photo of a beach → label: "Beach". The model doesn't tell you WHERE in the image the thing is — just what the image shows. Two sub-types: multi-class (exactly one label) and multi-label (multiple labels allowed per image).
Answering the question: "What objects are in this image and WHERE are they?" — identifies specific objects and draws bounding boxes around each one. Example: a photo of a street → detects: Car at [top-left], Person at [centre], Bicycle at [right]. Use when you need to locate and count specific objects, not just classify the whole image.
A category label applied to a training image. For classification: you add tags to whole images ("Cat", "Dog"). For object detection: you add tags to bounding box regions you draw on images. Tags are the categories your model learns to recognise. You need at least 5 images per tag minimum (15+ recommended for good accuracy).
Each image gets exactly one tag — the image belongs to one and only one category. The model outputs the single most likely class. Example: classify photos as either "Healthy" or "Diseased" plant — no image can be both. Use when categories are mutually exclusive.
Each image can have multiple tags — an image can belong to several categories simultaneously. Example: a food photo can be tagged as both "Vegetarian" and "Spicy" and "Main Course". The model outputs confidence scores for ALL tags, not just the top one. Use when categories are not mutually exclusive.
A trained version of your custom model. Each time you train in Custom Vision, a new Iteration is created — the previous iterations are kept. This gives you a version history. You can compare iterations' metrics side by side. Only one iteration can be published (active) at a time. If training with new data improves metrics, you publish the new iteration to replace the old one.
Of all the times the model said "Yes, this is a Cat" — what percentage was it actually right? Precision = True Positives / (True Positives + False Positives). High precision means: when the model fires, it's reliable. Low precision means: it fires too eagerly and generates many false alarms. Important when false positives are costly.
Of all the actual Cats in the test set — what percentage did the model correctly find? Recall = True Positives / (True Positives + False Negatives). High recall means: the model finds most of the real examples. Low recall means: it misses many real examples. Important when missing true positives is costly (e.g. disease detection).
The single summary metric for object detection models. Combines precision and recall across all object classes into one number. Higher is better — 100% mAP is perfect. Used because detecting objects requires both finding them (recall) AND putting the bounding box in the right place (precision). mAP is the standard metric for object detection; Precision and Recall are used for classification.
A confidence cutoff — the model only reports a detection/classification if its confidence score is above this threshold. Default: 50% (0.5). Raising the threshold (e.g. to 80%) → higher precision (fewer false positives) but lower recall (misses more). Lowering the threshold (e.g. to 30%) → higher recall (catches more) but lower precision (more false positives). The exam tests this tradeoff directly.
Quick Training: fast (minutes), uses a lightweight model, good for initial experiments and small datasets. Lower accuracy potential. No configuration needed. Advanced Training: slower (hours), uses a more powerful model, configures a training budget (how many hours of compute to use). Better accuracy, especially with more training data. The exam may ask when to use each.
The REST API endpoint for making predictions with your published Custom Vision model. After publishing an iteration, you get a prediction URL your application calls with new images. Two prediction methods: classify_image() for classification, detect_image() for object detection. The prediction URL and prediction key are separate from the training endpoint and key.
Custom Vision automatically suggests tags for newly uploaded untagged images based on the current model's predictions. You review and confirm or correct the suggestions rather than manually tagging every image from scratch. This speeds up the labelling process significantly as your dataset grows. The model learns from your corrections to improve future suggestions.
Choosing the right project type — the most common exam decision
Image Classification is like stamping a label on the whole photo envelope: "This envelope contains a cat photo." You know what's in the photo, but not where.
Object Detection is like shining a spotlight inside the photo: "There's a cat in the top-left corner, a dog in the centre, and a bowl in the bottom-right." You know both what and where.
If the scenario only needs to categorise images (spam vs not spam, defective vs normal), use classification.
If the scenario needs to find and locate specific things (count cars, detect defects in a specific part), use detection.
Decision guide — classification vs detection
| Scenario | Project Type | Why |
|---|---|---|
| Classify product images as "Defective" or "Normal" | Multi-class Classification | Each image is either one or the other — mutually exclusive categories |
| Tag food photos with all applicable dietary labels (Vegan, Spicy, Gluten-free) | Multi-label Classification | One photo can have multiple labels simultaneously |
| Detect and count cars in parking lot camera images | Object Detection | Need to find WHERE each car is and how many there are, not just "cars are present" |
| Locate and highlight specific defects in circuit board images | Object Detection | Need to know WHERE the defect is on the board — bounding box required |
| Identify which type of fruit is in a photo | Multi-class Classification | Image shows one type of fruit — one label per image |
| Detect and locate faces, cars, and pedestrians in dashcam footage | Object Detection | Multiple distinct object types need to be located with bounding boxes |
Labelling differences
1. Upload images to your project
2. Select each image (or batch)
3. Assign one tag (multi-class) or multiple tags (multi-label)
4. Repeat until all images are tagged
No drawing required — you just select the label for the whole image.
1. Upload images to your project
2. Open each image in the labelling tool
3. Draw a bounding box around each object
4. Assign a tag to each drawn box
5. Every object in the image must be boxed
More effort per image — but gives the model location data.
The complete Custom Vision pipeline
In customvision.ai (or via Python SDK), create a project specifying: project type (Classification or Object Detection), classification type (Multi-class or Multi-label), and domain (General, Food, Retail, Landmarks, etc. — each domain specialises the base model). Domains affect the underlying base model used for transfer learning. Choose "General" if none of the specific domains match.
Upload at least 5 images per tag (minimum). 15+ per tag recommended for reasonable accuracy. 50+ per tag for good production accuracy. Images should be varied — different angles, lighting conditions, backgrounds, sizes. Similar images don't help the model generalise. For object detection: draw bounding boxes around every instance of every object in each image.
Click "Train" — choose Quick Training (faster, less accurate) or Advanced Training (specify a compute budget in hours). Training performs transfer learning: it takes a powerful pre-trained vision model and fine-tunes it on your tagged images. A new Iteration is created. You can train multiple times — each creates a new Iteration.
After training, Custom Vision shows Precision, Recall, and mAP for the new Iteration. Review these metrics at different probability thresholds using the threshold slider. Identify which tags have low precision or recall — these need more training images or better image diversity. Compare against previous iterations to confirm improvement.
Use the "Quick Test" feature in the portal to upload images that were NOT part of training. Review the model's predictions — this is a real-world sanity check beyond the training metrics. If the model performs well on Quick Test images, you can be more confident in its real-world accuracy.
Click "Publish" on the Iteration you want to deploy. Give it a publication name. This exposes the prediction endpoint URL and prediction key that your application will use to call the model. Until published, the model is only accessible for testing in the portal — it has no REST endpoint for external applications.
Call the prediction endpoint from your application using the azure-cognitiveservices-vision-customvision Python SDK (or REST). For classification: call classify_image() — returns predictions with tag names and probabilities. For detection: call detect_image() — returns predictions with bounding boxes, tag names, and probabilities. Filter by probability threshold.
Precision, Recall, mAP — and the probability threshold tradeoff
"When the model fires, how often is it right?"
TP ÷ (TP + FP)
High precision = low false alarm rate. Relevant when false positives are expensive (e.g. flagging innocent products for disposal).
"Of all real examples, how many did it find?"
TP ÷ (TP + FN)
High recall = catches most real cases. Relevant when missing true positives is dangerous (e.g. missing a defect that could cause product failure).
Object detection summary metric. Combines precision + recall + bounding box accuracy across ALL classes into one score. Higher is better. Used for detection; Precision + Recall used for classification.
Probability threshold — the precision/recall tradeoff
Every prediction has a probability score (0–100%). The probability threshold is your cutoff — below it, the model stays silent. Adjusting it changes the precision/recall balance:
| Threshold | Effect on Precision | Effect on Recall | Use when… |
|---|---|---|---|
| High (e.g. 80%) | ⬆️ Higher (fewer false positives) | ⬇️ Lower (misses more real cases) | False alarms are very costly (e.g. triggering expensive automated responses) |
| Default (50%) | Balanced | Balanced | General purpose scenarios where both matter equally |
| Low (e.g. 30%) | ⬇️ Lower (more false positives) | ⬆️ Higher (catches more real cases) | Missing a real case is dangerous (e.g. missing a defect in safety-critical manufacturing) |
"The model is generating too many false alarms — legitimate products are being rejected."
→ This is a low Precision problem (too many false positives). Fix: raise the probability threshold.
"The model is missing too many actual defects — defective products are passing through."
→ This is a low Recall problem (too many false negatives). Fix: lower the probability threshold.
"The company needs to balance catching defects vs. not flagging good products."
→ Use the default 50% threshold or adjust based on the cost comparison.
Custom Vision end-to-end — train, publish, and predict
Install: pip install azure-cognitiveservices-vision-customvision
from azure.cognitiveservices.vision.customvision.training import CustomVisionTrainingClient
from azure.cognitiveservices.vision.customvision.training.models import (
ImageFileCreateBatch, # Wraps a batch of images with their tags for upload
ImageFileCreateEntry, # One image + its tag(s) in a batch
Region, # Bounding box for object detection (normalised 0.0–1.0)
)
from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient
from msrest.authentication import ApiKeyCredentials
# CustomVision uses msrest credentials, not azure-core credentials
import os
import time # For polling the training status
# ── Connection details ─────────────────────────────────────────────────
# Custom Vision has SEPARATE endpoints and keys for Training and Prediction
# Find them in customvision.ai → Settings (gear icon)
TRAINING_ENDPOINT = os.environ.get("CUSTOMVISION_TRAINING_ENDPOINT")
TRAINING_KEY = os.environ.get("CUSTOMVISION_TRAINING_KEY")
PREDICTION_ENDPOINT = os.environ.get("CUSTOMVISION_PREDICTION_ENDPOINT")
PREDICTION_KEY = os.environ.get("CUSTOMVISION_PREDICTION_KEY")
PREDICTION_RESOURCE_ID = os.environ.get("CUSTOMVISION_PREDICTION_RESOURCE_ID")
# Resource ID format: /subscriptions/xxx/resourceGroups/xxx/providers/...
# Needed when publishing an Iteration — links the model to the prediction resource
# msrest credential wraps the key — different from AzureKeyCredential used elsewhere
training_credentials = ApiKeyCredentials(in_headers={"Training-key": TRAINING_KEY})
prediction_credentials = ApiKeyCredentials(in_headers={"Prediction-key": PREDICTION_KEY})
# Training client: create projects, upload images, train, evaluate
trainer = CustomVisionTrainingClient(TRAINING_ENDPOINT, training_credentials)
# Prediction client: make predictions against published models
predictor = CustomVisionPredictionClient(PREDICTION_ENDPOINT, prediction_credentials)
# ══════════════════════════════════════════════════════════════════════
# PART 1 — TRAINING: Create project, add tags, upload images, train
# ══════════════════════════════════════════════════════════════════════
# Get the list of available domains — you must use a domain ID, not a name
# Domains: General, Food, Landmarks, Retail, General (compact), etc.
domains = trainer.get_domains()
# Find the General classification domain
general_domain = next(d for d in domains if d.name == "General" and d.type == "Classification")
# Create a new Classification project
# classification_type: "Multiclass" (one tag per image) or "Multilabel" (many tags per image)
project = trainer.create_project(
name = "plant-disease-classifier",
description = "Classifies plant leaves as Healthy or Diseased",
domain_id = general_domain.id,
classification_type = "Multiclass" # Each plant is either Healthy OR Diseased
)
project_id = project.id
print(f"Created project: {project.name} (ID: {project_id})")
# ── Create Tags (categories) ───────────────────────────────────────────
# Tags are the categories the model will learn to recognise
# You must create tags BEFORE uploading images
healthy_tag = trainer.create_tag(project_id, "Healthy")
diseased_tag = trainer.create_tag(project_id, "Diseased")
# Each tag gets a unique ID — use the ID when uploading images
print(f"Created tags: {healthy_tag.name} (ID: {healthy_tag.id}), {diseased_tag.name}")
# ── Upload images with tags ────────────────────────────────────────────
# Build a batch — more efficient than uploading one image at a time
# Batch size limit: 64 images per call
upload_batch = []
healthy_images = ["healthy_leaf_1.jpg", "healthy_leaf_2.jpg", "healthy_leaf_3.jpg"]
for image_path in healthy_images:
with open(image_path, "rb") as f:
image_bytes = f.read() # Read the image as binary
# ImageFileCreateEntry: wraps one image + its tags for the batch
# tag_ids: a list of tag IDs — for multiclass, use exactly one tag
upload_batch.append(ImageFileCreateEntry(
name = image_path,
contents= image_bytes,
tag_ids = [healthy_tag.id] # This image belongs to the Healthy category
))
diseased_images = ["diseased_leaf_1.jpg", "diseased_leaf_2.jpg", "diseased_leaf_3.jpg"]
for image_path in diseased_images:
with open(image_path, "rb") as f:
image_bytes = f.read()
upload_batch.append(ImageFileCreateEntry(
name = image_path,
contents= image_bytes,
tag_ids = [diseased_tag.id] # This image belongs to the Diseased category
))
# Upload the entire batch at once — more efficient than one-by-one
upload_result = trainer.create_images_from_files(
project_id,
ImageFileCreateBatch(images=upload_batch) # Wrap the list in a batch object
)
# Check if all images were successfully uploaded
if not upload_result.is_batch_successful:
# Some images may have failed (unsupported format, duplicate, too small)
for img in upload_result.images:
if img.status != "OK":
print(f"Failed: {img.source_url} — {img.status}")
else:
print(f"Uploaded {len(upload_batch)} images successfully.")
# ── Train the model ────────────────────────────────────────────────────
# train_project() starts training and returns an Iteration object
# training_type: "Regular" (Quick Training) — for faster/smaller models
# For Advanced Training: training_type="Advanced", budget_in_hours=1 (minimum: 1 hour)
iteration = trainer.train_project(project_id)
print(f"\nTraining started — Iteration: {iteration.id}")
# Training is async — poll until status changes from "Training" to "Completed"
while iteration.status == "Training":
print(" Still training...")
time.sleep(10) # Wait 10 seconds before checking again
# get_iteration() refreshes the iteration status from the server
iteration = trainer.get_iteration(project_id, iteration.id)
print(f"Training complete! Status: {iteration.status}")
# ── Evaluate metrics ───────────────────────────────────────────────────
# get_iteration_performance() returns the evaluation metrics for a trained iteration
performance = trainer.get_iteration_performance(
project_id,
iteration.id,
threshold = 0.5 # Probability threshold: only count predictions above 50%
)
# Overall model metrics
print(f"\n=== Model Performance (threshold=50%) ===")
print(f" Precision: {performance.precision:.1%}") # :.1% formats 0.923 as "92.3%"
print(f" Recall: {performance.recall:.1%}")
print(f" mAP: {performance.average_precision:.1%}") # Mean Average Precision
# Per-tag metrics — see which tags are weak
print("\nPer-tag metrics:")
for tag_perf in performance.per_tag_performance:
print(f" {tag_perf.name}: precision={tag_perf.precision:.1%} recall={tag_perf.recall:.1%}")
# If Healthy is 95% precision but Diseased is only 60% precision,
# you need more / better Diseased training images
# ── Publish the Iteration ──────────────────────────────────────────────
# After evaluating, publish the iteration so external apps can use it
# publish_name: a human-readable name for this published model
# prediction_id: the Azure resource ID of your prediction resource
trainer.publish_iteration(
project_id,
iteration.id,
publish_name = "plant-classifier-v1",
prediction_id = PREDICTION_RESOURCE_ID
)
print("\nIteration published as 'plant-classifier-v1'")
# ══════════════════════════════════════════════════════════════════════
# PART 2 — PREDICTION: Call the published model with new images
# ══════════════════════════════════════════════════════════════════════
# ── Classification prediction ──────────────────────────────────────────
with open("new_leaf.jpg", "rb") as f:
test_image = f.read()
# classify_image(): makes a classification prediction with a local image
# project_id: which project this image belongs to
# published_name: the name you gave when publishing the iteration above
# image_data: the image bytes
results = predictor.classify_image(
project_id = project_id,
published_name = "plant-classifier-v1", # Must match what you used in publish_iteration()
image_data = test_image
)
# results.predictions: list of all tags with their probability scores
# Sorted by probability descending — first item is the most likely prediction
print("\n=== Classification Predictions ===")
for prediction in results.predictions:
# prediction.tag_name: the label — prediction.probability: confidence (0.0 to 1.0)
print(f" {prediction.tag_name}: {prediction.probability:.1%}")
# Best prediction: use the first result (highest probability)
best = results.predictions[0]
if best.probability >= 0.7: # Our custom threshold: only act if 70%+ confident
print(f"\nDiagnosis: {best.tag_name} ({best.probability:.1%} confidence)")
else:
print("\nLow confidence — flag for human review")
# ── URL-based prediction (no local file needed) ─────────────────────────
# classify_image_with_no_store() for predictions that don't store the image
# classify_image_url() for URL-based images
from azure.cognitiveservices.vision.customvision.prediction.models import ImageUrl
url_results = predictor.classify_image_url(
project_id = project_id,
published_name = "plant-classifier-v1",
image_url = ImageUrl(url="https://example.com/leaf-photo.jpg")
)
print(f"\nURL prediction: {url_results.predictions[0].tag_name} — {url_results.predictions[0].probability:.1%}")
Python — Object Detection prediction (with bounding boxes)
# Object detection prediction uses detect_image() instead of classify_image()
# Returns both tag names AND bounding box coordinates for each detected object
# Assume you have an object detection project and published iteration
with open("warehouse_photo.jpg", "rb") as f:
detect_image = f.read()
# detect_image(): same structure as classify_image() but returns bounding boxes
detect_results = predictor.detect_image(
project_id = project_id,
published_name = "defect-detector-v1",
image_data = detect_image
)
THRESHOLD = 0.6 # Only report detections above 60% confidence
print("\n=== Object Detection Results ===")
for prediction in detect_results.predictions:
# Filter out low-confidence predictions using our threshold
if prediction.probability < THRESHOLD:
continue # skip this prediction — not confident enough
# prediction.bounding_box: NORMALISED coordinates (0.0 to 1.0, not pixels)
# Multiply by image width/height to get pixel coordinates
bb = prediction.bounding_box
# bb.left: x position of left edge as a fraction of image width
# bb.top: y position of top edge as a fraction of image height
# bb.width: width as a fraction of total image width
# bb.height: height as a fraction of total image height
print(f" {prediction.tag_name}: {prediction.probability:.1%}")
print(f" Box: left={bb.left:.2f} top={bb.top:.2f} w={bb.width:.2f} h={bb.height:.2f}")
# Example to convert to pixel coordinates (assuming 1920x1080 image):
# pixel_x = int(bb.left * 1920) → left edge in pixels
# pixel_y = int(bb.top * 1080) → top edge in pixels
Custom Vision — what the exam tests
1. Classification vs Object Detection decision — "Categorise the whole image" → Classification. "Find WHERE objects are + draw boxes" → Object Detection. This is the most common project type selection question.
2. Precision/Recall/threshold tradeoff — Raising threshold improves precision but hurts recall. Lowering threshold improves recall but hurts precision. Know which direction each scenario needs.
3. mAP is the object detection metric; Precision+Recall for classification — Don't mix them up. mAP = Mean Average Precision, specifically for detection. Classification uses Precision and Recall directly.
4. Minimum 5 images per tag — The minimum to even start training. 15+ is recommended. Always check if a scenario mentions insufficient training data.
5. Training vs Prediction: separate endpoints and keys — Custom Vision has TWO endpoints (training and prediction) with different keys. Training creates/manages the model; Prediction makes predictions. A common code question tests if you know which client and key to use for predictions.
6. Object detection bounding boxes are normalised (0.0–1.0) — Not in pixels. Multiply by the image dimensions to convert to pixels. Classification doesn't use bounding boxes at all.
Test your understanding
Which Custom Vision project type should be used and why?
C is correct. The key phrase is "identify products including their positions in the image." Object Detection provides bounding boxes for each detected product — telling the system exactly where each item is, what it is, and how many there are. Multi-label Classification (B/D) could assign multiple product labels to the whole image but would not differentiate between two items of the same product type or provide locations. Without bounding boxes, you can't count individual items reliably or distinguish which product is where.