Image Analysis App

Ma Mahalakshmi V Updated 19 Sep 2026
7 min read ·Lesson 215 of 224

Project 6: Build an Image Analysis Application

This project builds an application that classifies and extracts structured information from images, using the vision and multimodal techniques from Unit 18. The scenario is a product-photo intake pipeline for an e-commerce catalog: images arrive from sellers, and the system needs to extract structured attributes, flag quality issues, and detect policy violations before a listing goes live.

Scope and Design Decisions

The application takes an image (a URL or local file) and produces a structured record: detected product category, extracted visible attributes (color, apparent material, condition), a quality flag, and a content-policy flag. It does not attempt image generation or editing — this is a pure analysis pipeline.

Two decisions matter most:

  1. Every extraction is a structured output, never freeform description. A catalog pipeline needs machine-usable fields (category, color, condition), not a paragraph a downstream system would need to re-parse — this reuses the structured-output discipline from Unit 6, applied to vision input instead of text.
  2. Quality and policy checks are separate calls from attribute extraction. Bundling "what is in this image" with "is this image acceptable" into a single call makes failures ambiguous — a low-quality or policy-violating image should still be intelligible in the extraction step where possible, and each check should be independently retriable and independently loggable.

Structured Attribute Extraction

from openai import OpenAI
from pydantic import BaseModel
from enum import Enum

client = OpenAI()

class Condition(str, Enum):
    new = "new"
    like_new = "like_new"
    used_good = "used_good"
    used_fair = "used_fair"
    damaged = "damaged"

class ProductAttributes(BaseModel):
    category: str
    primary_color: str
    apparent_material: str | None
    condition: Condition
    visible_defects: list[str]

def extract_product_attributes(image_url: str) -> ProductAttributes:
    response = client.responses.parse(
        model="gpt-5.6-terra",
        input=[{
            "role": "user",
            "content": [
                {"type": "input_text", "text": (
                    "Extract catalog attributes from this product photo. If a "
                    "defect is visible (scratches, stains, tears, missing parts), "
                    "list it explicitly in visible_defects."
                )},
                {"type": "input_image", "image_url": image_url},
            ],
        }],
        text_format=ProductAttributes,
    )
    return response.output_parsed

The content list mixes an input_text block with an input_image block in a single user message — this is the standard multimodal message shape from Unit 18, and it matters that the instruction text is included alongside the image in the same message rather than as a separate system prompt turn, since the model needs both pieces of context together to ground its extraction in what it is actually looking at. Condition is an enum rather than a free string, which constrains the model to a fixed, known vocabulary that downstream catalog code can switch on directly without needing to normalize inconsistent phrasing like "gently used" versus "used - good condition."

visible_defects is a list rather than a single optional string because a real product photo can show multiple independent issues (a scratch and a missing button), and collapsing them into one field would force the model to either pick one or awkwardly concatenate — a list is the correct shape for "zero or more of a kind of thing," a modeling choice worth applying generally whenever a field could plausibly have more than one value.

Quality and Policy Checks

class ImageQualityCheck(BaseModel):
    is_acceptable_quality: bool
    quality_issues: list[str]
    is_policy_compliant: bool
    policy_concerns: list[str]

def check_image_quality_and_policy(image_url: str) -> ImageQualityCheck:
    response = client.responses.parse(
        model="gpt-5.6-terra",
        input=[{
            "role": "system",
            "content": (
                "You are a content moderator for an e-commerce image pipeline. "
                "Flag quality issues (blur, poor lighting, watermarks obscuring "
                "the product, wrong aspect ratio) and policy concerns (visible "
                "faces of bystanders, brand logos suggesting counterfeit goods, "
                "prohibited item categories) independently."
            ),
        }, {
            "role": "user",
            "content": [{"type": "input_image", "image_url": image_url}],
        }],
        text_format=ImageQualityCheck,
    )
    return response.output_parsed

Quality and policy are modeled as two independent boolean-plus-reasons pairs in the same schema rather than a single pass/fail flag, because a listing can fail one and pass the other — a perfectly compliant photo can still be too blurry to publish, and a sharp, well-lit photo can still show a counterfeit logo. Keeping them as separate fields, each with its own explanatory list, means the pipeline can route these two failure types to different remediation flows (ask the seller to retake the photo versus escalate to a trust-and-safety review).

Note: What counts as a policy violation is domain- and platform-specific, and the system prompt above is illustrative. A production deployment should encode the platform's actual content policy explicitly, ideally as a shared, versioned document referenced consistently across every moderation call rather than restated ad hoc in each prompt.

The Intake Pipeline

from dataclasses import dataclass

@dataclass
class IntakeResult:
    image_url: str
    attributes: ProductAttributes | None
    quality: ImageQualityCheck
    approved: bool

def process_listing_image(image_url: str) -> IntakeResult:
    quality = check_image_quality_and_policy(image_url)

    if not quality.is_policy_compliant:
        return IntakeResult(image_url=image_url, attributes=None, quality=quality, approved=False)

    attributes = extract_product_attributes(image_url)

    approved = quality.is_acceptable_quality and attributes.condition != Condition.damaged
    return IntakeResult(image_url=image_url, attributes=attributes, quality=quality, approved=approved)

The pipeline short-circuits on a policy violation before ever calling attribute extraction — there is no catalog value in extracting the color and material of an image that will be rejected outright, and skipping that call saves cost and avoids ever storing structured metadata about content that should not be in the system at all. Quality issues, by contrast, do not block extraction: a slightly blurry but otherwise policy-compliant photo can still usefully report its category and color, and the pipeline simply marks it as unapproved for publishing pending a better photo.

Batch Processing Multiple Angles

def process_listing(image_urls: list[str]) -> list[IntakeResult]:
    return [process_listing_image(url) for url in image_urls]

def summarize_listing(results: list[IntakeResult]) -> dict:
    approved_count = sum(1 for r in results if r.approved)
    all_defects = sorted({
        defect
        for r in results
        if r.attributes
        for defect in r.attributes.visible_defects
    })
    return {
        "total_images": len(results),
        "approved_images": approved_count,
        "needs_new_photos": approved_count == 0,
        "aggregated_defects": all_defects,
    }

A real listing typically has several photos, and summarize_listing aggregates across all of them rather than treating each image as an isolated decision — a listing is publishable if at least one photo is acceptable, and defects noticed in any single photo (a scratch visible only from one angle) should surface in the aggregated summary even if other angles look clean. This reflects a broader pattern in multimodal pipelines: individual-item analysis and cross-item aggregation are separate concerns and should be separate functions.

Testing Without Real Images

def test_policy_violation_skips_attribute_extraction(monkeypatch_calls):
    quality_fail = ImageQualityCheck(
        is_acceptable_quality=True,
        quality_issues=[],
        is_policy_compliant=False,
        policy_concerns=["Visible third-party brand logo"],
    )
    monkeypatch_calls(quality_fn=lambda url: quality_fail, attr_fn=None)
    result = process_listing_image("https://example.com/fake.jpg")
    assert result.approved is False
    assert result.attributes is None
    print("PASS: policy-violating images skip attribute extraction entirely")

def test_damaged_condition_is_not_approved(monkeypatch_calls):
    quality_ok = ImageQualityCheck(
        is_acceptable_quality=True, quality_issues=[],
        is_policy_compliant=True, policy_concerns=[],
    )
    attrs = ProductAttributes(
        category="jacket", primary_color="blue", apparent_material="denim",
        condition=Condition.damaged, visible_defects=["large tear on sleeve"],
    )
    monkeypatch_calls(quality_fn=lambda url: quality_ok, attr_fn=lambda url: attrs)
    result = process_listing_image("https://example.com/fake.jpg")
    assert result.approved is False
    print("PASS: damaged condition prevents approval even with acceptable quality")

def _make_monkeypatch():
    import builtins
    module = globals()
    originals = {}
    def apply(quality_fn=None, attr_fn=None):
        if quality_fn:
            originals["quality"] = module["check_image_quality_and_policy"]
            module["check_image_quality_and_policy"] = quality_fn
        if attr_fn:
            originals["attr"] = module["extract_product_attributes"]
            module["extract_product_attributes"] = attr_fn
    return apply

monkeypatch_calls = _make_monkeypatch()
test_policy_violation_skips_attribute_extraction(monkeypatch_calls)
test_damaged_condition_is_not_approved(monkeypatch_calls)

Both tests replace the two model-calling functions with lambdas that return hand-built Pydantic objects, letting process_listing_image's branching logic — the short-circuit on policy failure, the approval rule involving condition — be verified deterministically. This is the core benefit of keeping the model calls as separate, swappable functions: the orchestration logic that decides what to do with the model's output can be tested exhaustively without ever sending an image to the API.

Extending This Project

Add a duplicate-image detector using perceptual hashing to catch sellers reusing stock photos across different listings, and add a size-and-fit extraction pass for apparel categories that cross-references extracted attributes against a category-specific attribute schema.

Common Mistakes

  • Combining quality checks and attribute extraction into a single call. This makes it impossible to independently retry or reroute one without the other, and conflates two genuinely different failure categories that need different remediation paths.
  • Using free-text fields for attributes that have a fixed, known vocabulary. A condition or category field modeled as a free string produces inconsistent values across images that a downstream filter or search index cannot reliably group.
  • Running full attribute extraction on images that fail policy checks. This wastes a model call and, worse, risks persisting structured metadata about content the platform should not be storing at all.

Best Practices

  • Model quality and policy as independent flags, each with its own reasons. A photo can fail one without failing the other, and pipelines need to route each failure type differently.
  • Short-circuit downstream processing on a policy failure. Don't extract further information from content that will be rejected regardless of what else is found.
  • Aggregate across multiple images at the listing level, separate from per-image analysis. Keep the function that judges a single image distinct from the function that judges a listing as a whole.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Image Analysis App and get answers drawn from it.

Signed-in readers only.