Skip to content

Instantly share code, notes, and snippets.

@vmphase
Last active July 13, 2026 19:13
Show Gist options
  • Select an option

  • Save vmphase/7403d5630f716556898a50408a5e3781 to your computer and use it in GitHub Desktop.

Select an option

Save vmphase/7403d5630f716556898a50408a5e3781 to your computer and use it in GitHub Desktop.
Detecting repeated Discord scam images with Perceptual Hashing

Image-based scam campaigns have become a common moderation challenge across Discord communities.
A typical campaign follows a familiar pattern:

  • Either a random account joins a server, waits a few hours (sometimes days), and starts posting scam messages;
  • Or a legitimate account gets compromised and suddenly begins posting the exact same scam across multiple channels.

Regardless of how the campaign starts, one characteristic remains remarkably consistent: the media attachment.

Whether it's the well-known MrBeast scam, fake nitro giveaways, or other phishing attempts, attackers rarely generate entirely new assets. Instead, the same image or GIF is repeatedly reused, occasionally with small modifications such as different text, slight color adjustments, or additional overlays.

This presents a frustrating moderation problem. Traditional text-based filters are ineffective because the spam content is embedded inside an image, while OCR introduces additional computational complexity. As a result, moderation often remains largely manual. A simpler alternative is to compare the images themselves.

The approach

The goal is not to determine whether two files are byte-for-byte identical. Instead, the objective is to determine whether two images represent the same visual content. A good fit for this problem is perceptual hashing, specifically the difference hash (dHash). The algorithm is dead simple:

  1. Convert the image to grayscale.
  2. Resize it to a fixed resolution of (N+1) * N
  3. Compare every pixel with its immediate neighbour.
  4. Encode whether the brightness increased or decreased as a single bit.

For each pixel:

$$ h(x, y) = \begin{cases} 1, & I(x+1, y) > I(x, y) \\ 0, & \text{otherwise} \end{cases} $$

where (I(x, y)) is the grayscale intensity.
The resulting sequence of bits forms a compact fingerprint describing the image's structure rather than its exact pixels.

Comparing two hashes

Once both images are hashed, comparison becomes trivial. The Hamming distance tells us how many bits differ:

$$ d = \sum_{i=1}^{n} (h_{1,i} \ne h_{2,i}) $$

A normalized similarity score is then:

$$ \text{similarity} = 1 - \frac{d}{n} $$

where:

  • (d) is the Hamming distance,
  • (n) is the total number of bits.

A distance of 0 means the hashes are identical, while small distances generally indicate visually similar images, even if they were resized or lightly modified.

PoC

import numpy as np
from PIL import Image

HASH_SIZE = 16


def difference_hash(path: str) -> np.ndarray:
    img = (
        Image.open(path)
        .convert("L")
        .resize((HASH_SIZE + 1, HASH_SIZE), Image.Resampling.LANCZOS)
    )

    pixels = np.asarray(img, dtype=np.uint8)
    return pixels[:, 1:] > pixels[:, :-1]


def compare_images(path1: str, path2: str) -> dict[str, bool | int | float]:
    hash1 = difference_hash(path1)
    hash2 = difference_hash(path2)

    distance = int(np.count_nonzero(hash1 != hash2))
    bits = hash1.size
    similarity = float(1.0 - distance / bits)

    return {
        "match": similarity >= 0.7,
        "distance": distance,
        "bits": bits,
        "similarity": similarity,
    }

The threshold is intentionally conservative for the proof of concept.
In practice it should be tuned based on real-world data and acceptable false-positive rates.

Results

For two identical images:

{ 
    "match": True, 
    "distance": 0, 
    "bits": 256, 
    "similarity": 1.0, 
}

For the same images with modified colours and different text:

{ 
    "match": True, 
    "distance": 92, 
    "bits": 256, 
    "similarity": 0.740625, 
}

This is by no means a complete anti-scam solution, but it provides a lightweight signal that can be combined with behavioural heuristics to detect coordinated image-based scam campaigns with very little computational overhead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment