# Image Comparator (`noisy_alchemy/image-comparator`) Actor

Compare a source image against multiple targets using deep-learning model to determine visual similarity. Ideal for e-commerce matching, copyright detection, and image deduplication. Accepts both URLs and Base64 encoded images to provide highly accurate similarity scoring.

- **URL**: https://apify.com/noisy\_alchemy/image-comparator.md
- **Developed by:** [Aljeandro](https://apify.com/noisy_alchemy) (community)
- **Categories:** AI, Other, Agents
- **Stats:** 11 total users, 2 monthly users, 100.0% runs succeeded, 3 bookmarks
- **User rating**: 4.53 out of 5 stars

## Pricing

from $1.00 / 1,000 image processeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

<div align="center">

## 🔍 Image Comparator

#### AI-Powered Visual Similarity

**Instantly compare images and get a similarity score + match verdict.**<br/>
Works with public URLs **and** base64-encoded images — no preprocessing needed.

[![Apify Actor](https://img.shields.io/badge/Apify-Actor-blueviolet?style=for-the-badge\&logo=apify\&logoColor=white)](https://apify.com)
[![ONNX Runtime](https://img.shields.io/badge/ONNX-Powered-blue?style=for-the-badge)](https://onnxruntime.ai/)
[![AI Vision](https://img.shields.io/badge/AI_Vision-Fast-success?style=for-the-badge)](#)

</div>

***

### 📑 Table of Contents

- [✨ What it does](#-what-it-does)
- [💡 Use cases](#-use-cases)
- [💰 Pricing](#-pricing)
- [📊 How similarity scoring works](#-how-similarity-scoring-works)
- [🚀 Performance](#-performance)
- [⚠️ Limitations](#️-limitations)
- [🛡️ Handling WAF & Rate Limits](#️-handling-waf--rate-limits)
- [🔌 Integrations & API usage](#-integrations--api-usage)
- [📋 Input Description](#-input-description)
- [🔲 Grayscale Mode (In Depth)](#-grayscale-mode-in-depth)
- [📤 Output Description](#-output-description)

***

### ✨ What it does

> This actor uses a state-of-the-art **deep learning vision model** to measure how visually similar two images are. It converts each image into a high-dimensional embedding vector, then computes the **cosine similarity** between them.

You provide:

| Input | Description |
|---|---|
| **1 source image** | The reference image you want to compare against |
| **1 or more target images** | The images you want to check for similarity |

You get back:

| Output | Description |
|---|---|
| **similarityScore** | A float between 0.0 and 1.0 (higher = more similar) |
| **isSimilar** | `true` / `false` — whether the score meets your threshold |

***

### 💡 Use cases

- **Duplicate detection** — Find near-duplicate product images across catalogs.
- **Brand monitoring** — Check if scraped images match your reference assets.
- **Content moderation** — Flag re-uploaded or slightly altered images.
- **Visual QA** — Verify screenshots or rendered pages against golden references.
- **E-commerce matching** — Match supplier images to your existing product photos.

***

### 💰 Pricing

This actor uses **pay-per-event** pricing based on the total number of comparisons made, plus a minimal base charge for the actor start.

| Event | Metric | Rate |
|---|---|---|
| **Actor Start** | `apify-actor-start` | **$0.00005 per run** (covers the first 5s of compute) |
| **Image Processed** | `image-processed` | **$1.00 per 1,000 comparisons** ($0.00100 per comparison) |

**Examples:**

| Scenario | Total comparisons | Comparison Cost | Actor Start Cost | Total Cost |
|---|---|---|---|---|
| 1 source vs 1 target | 1 (min. 2 billed) | $0.00200 | $0.00005 | $0.00205 |
| 1 source vs 10 targets | 10 | $0.01000 | $0.00005 | $0.01005 |
| 1 source vs 100 targets | 100 | $0.10000 | $0.00005 | $0.10005 |
| 1 source vs 1,000 targets | 1,000 | $1.00000 | $0.00005 | $1.00005 |

***

### 📊 How similarity scoring works

| Score range | Interpretation |
|---|---|
| **0.95 – 1.00** | Near-identical or the same image (possibly resized/compressed). |
| **0.85 – 0.95** | Very similar — same subject, minor differences (angle, lighting). |
| **0.70 – 0.85** | Related — same category or concept, but visually distinct. |
| **Below 0.70** | Different images. |

***

### 🚀 Performance

| Metric | Value |
|---|---|
| **Model load** | Instant — weights are pre-baked into the Docker image |
| **Per-image embedding** | ~20–50 ms (CPU) / ~5–10 ms (GPU) |
| **Concurrency** | Up to 10 images downloaded in parallel |
| **Batch processing** | Images are embedded in batches of 16 for throughput |

#### Expected Execution Times (1-to-1 comparison)

On standard Apify CPU instances, you can expect the following benchmarks for a single source vs. target comparison:

- **~0.3 to 0.5 seconds ("Hot" Start)** — If the exact same URLs/Base64 strings have been run before, the actor instantly grabs the embeddings from the local cache.
- **~1 to 2 seconds ("Cold" Start)** — The lightweight ONNX model loads instantly from disk (< 50MB), downloads the images, and computes embeddings via vectorized numpy arrays. No more 15+ second HuggingFace model download delays!

> \[!NOTE]
> **Note on Batches:** If you run large batches (e.g., 1 source vs 100 targets), the sub-second model loading time is a flat fee, and the remaining 100 images will process rapidly in memory.

The actor is designed for **speed**:

1. **Pure ONNX Runtime** — The massive PyTorch dependency has been stripped out. The AI model runs on a highly optimized ONNX graph, cutting RAM usage to < 500MB and starting instantly.
2. **Async image fetching** — All images are downloaded concurrently using `httpx` with a configurable semaphore and standard browser User-Agents to bypass strict CDNs.
3. **Vectorized Math** — Cosine similarity is computed simultaneously across all images using a single, blazing-fast `numpy.dot()` matrix instruction.
4. **Resilience** — Malformed images, 404s, or 100MB+ files are automatically skipped and logged as errors without crashing your batch run.
5. **Smart Embedding Cache** — Automatically saves computed embeddings in an Apify Key-Value Store (`embedding-cache`). If you run the actor again with the same URLs or base64 images, it instantly reuses the cached data—skipping the download and inference steps entirely to save massive amounts of processing time.

***

### ⚠️ Limitations

- **Image format** — Supports JPEG, PNG, WebP, BMP, TIFF. Animated GIFs use the first frame only.
- **Image size** — Very large images (>20 MP) may slow down processing. The model internally resizes to 224×224 px.
- **Pixel-level diff** — This actor measures **semantic** similarity (does it look like the same thing?), not pixel-exact differences. Two photos of the same cat from different angles will score high; a red square and a blue square will score low, even if they're the same shape.
- **Timeout** — Image downloads time out after 30 seconds per image.

***

### 🛡️ Handling WAF & Rate Limits

When comparing a large batch of image URLs hosted on the same domain, the target server's Web Application Firewall (WAF) like Cloudflare or AWS WAF might block the requests (e.g. returning `403 Forbidden` or `429 Too Many Requests`). Since this actor downloads images concurrently to maximize speed, it can inadvertently trigger these protections.

To combat WAF problems when processing lots of images, you have four options:

1. **Use Base64 Encoding (Recommended):** Download the images inside your own scraper (where you can easily use Apify Proxies, Playwright, or session rotation) and pass the images to this actor as **base64-encoded strings**. This completely bypasses all downloading within the actor.
2. **Host on Cloud Storage:** Temporarily upload your scraped images to an AWS S3 bucket, Google Cloud Storage, or an Apify Key-Value store, and pass those unblocked public URLs to the actor.
3. **Limit Batch Size:** Break your comparisons into smaller batches (e.g. 10-20 images at a time) rather than sending 1,000 URLs from the same domain in a single run.
4. **Hybrid "Distributed Fetching" Strategy:** You can freely mix URLs and Base64 strings in the exact same `targetImages` array. By offloading some of the downloads to your users' PCs (converting them to Base64 locally using their residential IPs) and leaving the rest as URLs for the actor to download, you dramatically reduce the traffic volume coming from the actor's server, heavily minimizing the chance of triggering the WAF.

> \[!IMPORTANT]
> **The Golden Rule of Rate Limits:** If you keep your batches under **20–50 image URLs per domain per minute**, you can often fly under the radar of standard WAF rate limits. However, if you try to process 500+ URLs from the exact same website all at once, you will almost certainly trigger a WAF block within the first few seconds.

#### 🛡️ Built-in Defenses (What the Code Does Under the Hood)

Even before you apply any of the strategies above, the actor already employs several code-level techniques to minimize the chance of triggering WAFs:

| Defense | How it works |
|---|---|
| **Randomized Browser TLS Fingerprinting** | Every image request randomly selects a real browser TLS profile (`chrome120`, `safari17_0`, `edge101`, etc.) via `curl_cffi` + `libcurl-impersonate`. The server sees what looks like a genuine browser connection — not a Python script. |
| **Authentic Browser Headers (`browserforge`)** | Each request generates a full set of modern browser headers (`Sec-Ch-Ua`, `Sec-Fetch-Dest: image`, `Accept: image/avif,image/webp,...`) matching the chosen browser profile, so even header-based fingerprinting checks pass. |
| **Per-Domain Anti-Rate-Limit Micro-Jitter** | Consecutive requests to the **same domain** are automatically spaced with a small random delay (20–80 ms) via the `DomainSessionManager`. This prevents the "machine-gun" burst pattern that CDNs like Cloudflare flag instantly. |
| **Per-Domain Connection Pooling** | HTTP/2 sessions are pooled and reused per host domain, eliminating repeated TLS handshakes. This looks like a normal browser keeping a connection alive — not opening hundreds of new sockets. |
| **Automatic Retry with Proxy Rotation** | If a download fails (e.g. `403` or `429`), the actor automatically retries up to 3 times, each time with a **fresh random browser profile** and — if Apify Proxy is configured — a **brand new proxy IP**. |
| **Bounded Concurrent Downloads** | Downloads are capped at 10 parallel requests (configurable via `MAX_CONCURRENT_DOWNLOADS`) using an asyncio semaphore. This prevents flooding a single server with dozens of simultaneous connections. |
| **Custom Headers & Referer Per Target** | You can pass per-image `headers` and `Referer` values (e.g. to bypass hotlink protection) that get merged on top of the auto-generated browser headers for that specific request. |

***

### 🔌 Integrations & API usage

Call this actor from any language via the [Apify API](https://docs.apify.com/api/v2):

```bash
curl -X POST "https://api.apify.com/v2/acts/%3CYOUR_ACTOR_ID%3E/runs?token=%3CYOUR_TOKEN%3E" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceImage": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
    "targetImages": ["https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg"]
  }'
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_TOKEN>")
run = client.actor("<YOUR_ACTOR_ID>").call(run_input={
    "sourceImage": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
    "targetImages": [
        "https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg",
    ],
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    for result in item["results"]:
        print(f"Target #{result['targetIndex']}: "
              f"score={result['similarityScore']:.4f}  "
              f"match={result['isSimilar']}")
```

#### JavaScript / Node.js

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_TOKEN>' });

const run = await client.actor('<YOUR_ACTOR_ID>').call({
    sourceImage: 'https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg',
    targetImages: [
        'https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg',
    ],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
items[0].results.forEach((r) => {
    console.log(`Target #${r.targetIndex}: score=${r.similarityScore} match=${r.isSimilar}`);
});
```

***

### 💬 Support & Feedback

Found a bug or have a feature request? Reach out via the Apify issues page.

***

***

## 📋 Input Description

Detailed reference for every input field accepted by this actor.

***

#### 🖼️ Source Image

`sourceImage`

**Optional**

The reference image to compare all target images against. This is the "baseline" — every target will be scored based on how similar it is to this image.

Accepts two formats:

- **Public URL** — Any directly accessible image URL (e.g., `https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg`)
- **Base64-encoded string** — With or without the `data:image/...;base64,` prefix

> \[!TIP]
> **Pro tip:** For fastest results, use URLs pointing to images hosted on a CDN or fast server. Base64 input avoids the network download but increases the payload size.

> \[!NOTE]
> If left empty, a sample image will be used for demonstration purposes. This is useful for testing the actor before integrating it into your pipeline.

**Type:** `string`

**Default:**

```
https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg
```

**Example (URL):**

```
https://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Orange_tabby_cat_sitting_on_fallen_leaves-Hisashi-01A.jpg/1200px-Orange_tabby_cat_sitting_on_fallen_leaves-Hisashi-01A.jpg
```

**Example (base64 with data-URI):**

```
data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...
```

**Example (raw base64):**

```
/9j/4AAQSkZJRgABAQAAAQABAAD...
```

***

#### 🎯 Target Images

`targetImages`

**Optional**

A list of one or more images to compare against the source image. Each item in the array is scored independently — you get one result object per target.

Each array element accepts the same two formats as `sourceImage`:

- **Public URL**
- **Base64-encoded string** (with or without data-URI prefix)

You can freely mix URLs and base64 strings in the same array.

> \[!TIP]
> **Pro tip:** For **1-to-1 comparison**, pass a single-element array. For **1-to-N batch comparison**, pass as many targets as you need — they are all processed in a single run with batched inference for maximum speed.

> \[!WARNING]
> **Pricing note:** You are charged based on the **total number of comparisons** made (which is equal to the number of targets). See the Pricing section above.

> \[!NOTE]
> **Blank entries:** Any blank or empty strings in the array are automatically filtered out. If all entries are blank, the default sample images will be used.

**Type:** `array` of `string`

**Default:**

```json
[
    "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/1200px-Cat_November_2010-1a.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/YellowLabradorLooking_new.jpg/1200px-YellowLabradorLooking_new.jpg"
]
```

**Example (URLs only):**

```json
[
    "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Kittyply_edit1.jpg/1200px-Kittyply_edit1.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/GoldenGateBridge-001.jpg/1200px-GoldenGateBridge-001.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Camponotus_flavomarginatus_ant.jpg/1200px-Camponotus_flavomarginatus_ant.jpg"
]
```

**Example (mixed URLs + base64):**

```json
[
    "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Kittyply_edit1.jpg/1200px-Kittyply_edit1.jpg",
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
    "/9j/4AAQSkZJRgABAQAAAQABAAD..."
]
```

***

#### 🎚️ Threshold

`threshold`

**Optional**

The similarity score cutoff (between `-1.0` and `1.0`) that determines whether `isSimilar` is `true` or `false`. Any target image that scores equal to or higher than this value will be marked as a match.

> **💡 Pro tip:** A threshold of `0.85` works great for detecting near-duplicates or identical subjects from different angles. Increase it to `0.95` for strict exact-duplicate detection.

**Type:** `number`

**Default:** `0.85`

***

***

#### 🔲 Grayscale Mode

`grayscale`

**Optional**

When set to `true`, all images (source and targets) are converted to **grayscale** before the AI computes their embeddings. This strips all colour information so the model focuses purely on **shapes, textures, edges, and structural patterns**.

**Type:** `boolean`

**Default:** `false`

**Example:**

```json
{
    "sourceImage": "https://example.com/product-daylight.jpg",
    "targetImages": ["https://example.com/product-studio.jpg"],
    "grayscale": true
}
```

***

### 🔲 Grayscale Mode (In Depth)

This section explains exactly what Grayscale Mode does, how it works under the hood, and — most importantly — when you should and shouldn't use it.

#### How it works technically

When `"grayscale": true` is set, every image goes through this transformation **before** being fed to the AI model:

```
Original Image (RGB)  →  Convert to Luminance (L)  →  Convert back to RGB
     (R,G,B)                  (single channel)          (R=G=B=Gray)
```

1. **`convert("L")`** — The image is converted to a single-channel luminance image using the standard ITU-R 601-2 formula: `L = 0.299R + 0.587G + 0.114B`. This is how the human eye perceives brightness.
2. **`convert("RGB")`** — The single gray channel is duplicated into all three RGB channels (R=G=B=Gray). This ensures the image tensor stays the correct shape `(3, 224, 224)` for the ResNet-18 model.
3. The ImageNet normalization and ONNX inference proceed as usual.

The result is that the AI model only sees brightness/contrast/texture patterns — it has **zero colour information** to work with.

> \[!IMPORTANT]
> **Cache isolation:** Grayscale and colour embeddings for the same image URL are stored under **different cache keys**. A URL processed with `grayscale: true` will never return a cached colour embedding, and vice versa. This prevents incorrect similarity scores when switching between modes.

#### ✅ When to USE Grayscale Mode

| Scenario | Why grayscale helps |
|---|---|
| **Different lighting conditions** | A product photographed under warm tungsten light vs. cool daylight will have very different colour casts. Grayscale removes this tint so the model correctly identifies them as the same product. |
| **Cross-platform image matching** | The same photo displayed on different screens, printed on paper, or saved with different colour profiles may have shifted colours. Grayscale normalizes all of these. |
| **Coloured vs. monochrome versions** | Matching a full-colour logo to its black-and-white variant, or a colour photo to its sepia-toned version. |
| **Recoloured products** | If a red t-shirt and a blue t-shirt of the exact same design should be considered "the same product", grayscale ensures the colour difference doesn't reduce the similarity score. |
| **Architectural / blueprint comparisons** | When comparing floor plans, wireframes, or technical drawings where the content is the same but rendered in different colour schemes. |
| **Night vs. day shots** | Photos of the same location taken at different times of day often have dramatically different colour palettes but similar structures. |

**Real-world example:**

> You have a product image taken by your photographer (warm studio lighting, orange tint) and a supplier image of the same product (cool warehouse lighting, blue tint). In standard mode, similarity might score **0.78**. With grayscale mode enabled, the same pair scores **0.92** — correctly identifying them as the same product.

#### 🔬 Empirical Test Results

To demonstrate the effectiveness of this feature, we ran a live test using a [product image from Amazon](https://m.media-amazon.com/images/I/814DIsC3ddL._AC_SY300_SX300_QL70_ML2_.jpg):

1. **Grayscale vs. Grayscale** (comparing the image to itself, both grayscaled)
   - **Score:** `1.000000` (Perfect match, as expected)
2. **RGB (Standard) vs. Grayscale** (comparing the full-color image against the grayscaled version)
   - **Score:** `0.991756`
   - *Takeaway:* Even with 100% of the color information stripped from the target image, the model still recognizes it with a massive **99.1% similarity score**.

#### ❌ When NOT to use Grayscale Mode

| Scenario | Why grayscale is a bad idea |
|---|---|
| **Colour IS the distinguishing feature** | A red apple and a green apple are structurally identical. In grayscale, they would score ~0.99 similarity — a false positive if you need to distinguish ripe from unripe. |
| **UI/screenshot testing** | A "Submit" button turning from green (success) to red (error) is a critical state change. Grayscale mode would make both states look identical. |
| **Brand colour verification** | If you're checking whether a brand's logo uses the correct Pantone colours, grayscale would defeat the purpose entirely. |
| **Art / design comparisons** | Two paintings with the same composition but wildly different colour palettes (e.g. Monet's cathedral series) should be treated as different works. |
| **General-purpose similarity** | For most use cases, the standard colour mode produces the most human-intuitive results. Only enable grayscale when you have a specific reason to ignore colour. |

> \[!TIP]
> **Not sure?** Run the same comparison twice — once with `"grayscale": false` (default) and once with `"grayscale": true`. Compare the scores. If the grayscale scores are consistently closer to what you expect, enable it for your pipeline.

#### Summary

| Mode | Compares | Best for |
|---|---|---|
| **Standard** (default, `grayscale: false`) | Colour + shape + texture | General-purpose similarity, brand verification, UI testing |
| **Grayscale** (`grayscale: true`) | Shape + texture only | Cross-lighting matching, recoloured products, monochrome variants |

***

#### 🆔 Custom IDs (Optional)

`customId` / `sourceImageId` / Target objects with `customId`

You can assign custom tracking IDs to the top-level request, the source image, and individual target images.

- **Request ID (`customId`):** Track whole batch/request operations.
- **Source Image ID (`sourceImageId`):** Identify source images in your internal systems.
- **Target Image ID (`customId` in object):** Assign custom IDs to individual target items instead of relying solely on array index.

***

#### 🌐 Apify Proxy Support (`proxyConfiguration`)

Optionally route all image download requests through **Apify Proxies** (Datacenter or Residential) to solve IP-level rate limits:

```json
{
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": ["RESIDENTIAL"]
    }
}
```

***

#### 🎲 Randomized Browser TLS Impersonation & Auto-Retry

Image downloads are powered by `curl_cffi` built with `libcurl-impersonate`:

- **Random Signature Rotation:** Every request automatically selects a random browser TLS fingerprint (`chrome120`, `safari17_0`, `edge101`, `safari15_3`, etc.).
- **Automatic Proxy IP Switching on `403`:** If a request receives a `403 Forbidden` or `429 Too Many Requests`, the actor automatically requests a fresh Proxy IP from Apify Proxy for the next retry attempt alongside a new browser signature.

***

#### ⚡ High-Performance Browser Simulation & Connection Engine

Image downloading includes:

1. **`browserforge` Authentic Header Generation:** Dynamically generates modern browser headers (`Sec-Ch-Ua`, `Sec-Fetch-Dest: image`, `Sec-Fetch-Mode: no-cors`, `Accept: image/avif,image/webp,...`) matching the selected browser profile (`chrome`, `safari`, `edge`).
2. **Per-Domain Connection Pooling:** Pools `AsyncSession` HTTP/2 connections per host domain, eliminating repeated TLS handshakes and doubling download throughput.
3. **Anti-Rate-Limit Micro-Jitter:** Injects micro-delays (20–80 ms) for consecutive requests to the same host domain to avoid triggering CDN rate limits during large batch processing.

***

#### 🔑 Custom HTTP Headers & Referer

Pass custom HTTP headers globally or per target image to bypass hotlink protections or pass authentication tokens:

```json
{
    "headers": {
        "User-Agent": "Custom-Agent/1.0",
        "Referer": "https://example.com/"
    },
    "targetImages": [
        {
            "url": "https://example.com/protected.jpg",
            "customId": "target_01",
            "headers": {
                "Cookie": "session_id=xyz123"
            }
        }
    ]
}
```

***

## 📥 Input Example

Full JSON input with custom IDs:

<details>
<summary><b>Click to expand full JSON Input Example</b></summary>

```json
{
    "customId": "req_2026_001",
    "sourceImage": {
        "url": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
        "customId": "src_catalog_product"
    },
    "targetImages": [
        "https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg",
        "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."
    ]
}
```

</details>

#### Minimal input (uses all defaults)

```json
{}
```

#### 1-to-1 comparison

```json
{
    "sourceImage": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Orange_tabby_cat_sitting_on_fallen_leaves-Hisashi-01A.jpg/1200px-Orange_tabby_cat_sitting_on_fallen_leaves-Hisashi-01A.jpg",
    "targetImages": [
        "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Kittyply_edit1.jpg/1200px-Kittyply_edit1.jpg"
    ]
}
```

#### 1-to-N comparison with custom threshold

```json
{
    "sourceImage": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
    "targetImages": [
        "https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg"
    ]
}
```

#### Grayscale comparison (ignore colour differences)

```json
{
    "sourceImage": "https://example.com/product-photo-daylight.jpg",
    "targetImages": [
        "https://example.com/product-photo-studio.jpg",
        "https://example.com/product-photo-outdoor.jpg"
    ],
    "grayscale": true
}
```

#### Pure base64 input (no URLs)

```json
{
    "sourceImage": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
    "targetImages": [
        "/9j/4AAQSkZJRgABAQAAAQABAAD..."
    ]
}
```

#### Hybrid / Mixed input (Base64 + URLs)

```json
{
    "sourceImage": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
    "targetImages": [
        "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...",
        "https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg",
        "data:image/png;base64,iORw0KGgoAAAANSUhEUg..."
    ]
}
```

***

***

## 📤 Output Description

The actor pushes a single JSON object to the default dataset. Below is the detailed schema of every output field.

***

#### sourceImage

**Required**

Truncated echo of your source image input (max 120 characters). Useful for identifying which source was used when reviewing results.

**Type:** `string`

**Example:**

```
https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg
```

***

#### totalTargets

**Required**

The number of target images that were compared against the source.

**Type:** `integer`

**Example:**

```
3
```

***

#### totalImagesProcessed

**Required**

The total number of images processed in this run (1 source + N targets). This is the number used for billing.

**Type:** `integer`

**Example:**

```
4
```

***

#### results

**Required**

Array of comparison results — one entry per target image.

**Type:** `array` of `object`

Each object in the array contains:

##### results\[].targetIndex

**Required**

Zero-based index matching the position in your `targetImages` input array. Use this to map results back to your original input list.

**Type:** `integer`

**Example:** `0`

***

##### results\[].targetImage

**Required**

Truncated echo of the target image input (max 120 characters). For base64 inputs, this will be the first 120 characters of the encoded string.

**Type:** `string`

**Example:**

```
https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/1200px-Cat_November_2010-1a.jpg
```

***

##### results\[].similarityScore

**Required**

Cosine similarity between the source and this target image. Ranges from `0.0` (completely different) to `1.0` (identical). This is the raw AI confidence score.

**Type:** `number`

**Minimum:** `0.0`

**Maximum:** `1.0`

**Example:** `0.943218`

***

##### results\[].isSimilar

**Required**

Boolean verdict: `true` if `similarityScore >= 0.85` (our internal similarity threshold), otherwise `false`. This is the quick yes/no answer — use `similarityScore` for more nuanced decisions.

**Type:** `boolean`

**Example:** `true`

***

#### error

**Optional** *(only present when the run fails)*

Human-readable error message describing what went wrong. Present instead of the normal output fields when the actor encounters an unrecoverable error.

**Type:** `string`

**Example:**

```
HTTP error fetching image: 404 – https://upload.wikimedia.org/wikipedia/commons/nonexistent-image.jpg
```

***

***

## 📤 Output Example

#### Successful run — 1 source vs 2 targets

<details>
<summary><b>Click to expand full JSON Output Example</b></summary>

```json
{
    "sourceImage": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
    "totalTargets": 1,
    "totalImagesProcessed": 2,
    "results": [
        {
            "targetIndex": 0,
            "targetImage": "https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg",
            "similarityScore": 0.743218,
            "isSimilar": false
        }
    ]
}
```

</details>

#### Successful run — 1 source vs 1 target (exact duplicate)

```json
{
    "sourceImage": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Orange_tabby_cat_sitting_on_fallen_leaves-Hisashi-01%E2%80%A6",
    "totalTargets": 1,
    "totalImagesProcessed": 2,
    "results": [
        {
            "targetIndex": 0,
            "targetImage": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Kittyply_edit1.jpg/1200px-Kittyply_edit1.jpg",
            "similarityScore": 0.998714,
            "isSimilar": true
        }
    ]
}
```

#### Failed run — bad URL

```json
{
    "error": "HTTP error fetching image: 404 – https://upload.wikimedia.org/wikipedia/commons/nonexistent-image.jpg"
}
```

# Actor input Schema

## `customId` (type: `string`):

Optional custom identifier for tracking this request/batch in output data.

## `sourceImageId` (type: `string`):

Optional custom ID for the source image.

## `sourceImage` (type: `string`):

The reference image to compare against. Accepts a public URL or base64 string (or an object with {'url': '...', 'customId': '...'}).

## `targetImages` (type: `array`):

A list of images to compare with the source. Each item can be a public URL, a base64 string, or an object with {'url': '...', 'customId': '...', 'headers': {...}}.

## `proxyConfiguration` (type: `object`):

Select Apify Proxies (Datacenter or Residential) to route image downloads through.

## `headers` (type: `object`):

Optional HTTP headers to include with all image download requests (e.g. {'Referer': '...', 'Cookie': '...'}).

## `threshold` (type: `number`):

The similarity score cutoff (between -1.0 and 1.0) that determines whether isSimilar is true or false. Any target image scoring at or above this value is marked as a match.

## `grayscale` (type: `boolean`):

When enabled, all images are converted to grayscale before comparison. This strips colour information so the AI model focuses purely on shapes, textures, and structural patterns — useful when lighting or colour variations cause false negatives. Default: false.

## Actor input object example

```json
{
  "sourceImage": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
  "targetImages": [
    "https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg"
  ],
  "threshold": 0.85,
  "grayscale": false
}
```

# Actor output Schema

## `resultsDataset` (type: `string`):

The default dataset containing the comparison scores.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "sourceImage": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
    "targetImages": [
        "https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg"
    ],
    "threshold": 0.85
};

// Run the Actor and wait for it to finish
const run = await client.actor("noisy_alchemy/image-comparator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "sourceImage": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
    "targetImages": ["https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg"],
    "threshold": 0.85,
}

# Run the Actor and wait for it to finish
run = client.actor("noisy_alchemy/image-comparator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sourceImage": "https://m.media-amazon.com/images/I/51N+ZjdF45L._AC_SX679_.jpg",
  "targetImages": [
    "https://m.media-amazon.com/images/I/81BmfKx3upL._AC_SX679_.jpg"
  ],
  "threshold": 0.85
}' |
apify call noisy_alchemy/image-comparator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,noisy_alchemy/image-comparator"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/XdPa8MRTb1EQ8J3cA/builds/pAtf8rMlYv162YHbq/openapi.json
