Multiple gpt-image-1 high fidelity edits lead to grainy result

And here it is. Python to accept a base64 return from the API, and flag it if it has the artifacts damaging the output.

“sharpened‑edge” artifact detector

Key ideas

Signal Why it helps Scale in score
Sharpness / Tenengrad Oversharpened swirl images have 5‑20× the high‑frequency energy of a normal photo. 0 – 0.45
Edge‑pixel ratio Fault frames are almost all edges. 0 – 0.25
Inverse saturation They are nearly monochrome. 0 – 0.12
Extreme gray share Huge portion of pixels are ≈ 0 or 255. 0 – 0.10
Inverse colourfulness Natural scenes are far more colourful. 0 – 0.08

On the images from this forum thread

good_house      score=0.366  artifact=False
bad_house       score=0.893  artifact=True
good_family     score=0.175  artifact=False
bad_family      score=0.891  artifact=True
good_biophage   score=0.226  artifact=False
bad_biophage    score=0.763  artifact=True

Damaged pictures are confidently caught; clean photographs pass. 0.70 seems a good threshold, unless you are making pictures of a thousand zebras.

Feel free to tweak the weightings that are internal if you want to log or return thresholds also, against more samples – but this configuration should already work robustly on the characteristic high‑contrast, swirly edge failure mode. You can give at least one or two automated retries, relying on the model’s lack of temperature or seed control to do something different.

A readable function, for what was developed as a class, by GPT-4.5

Here’s a clearly refactored, human-readable version of the function, with meaningful variable names and explanatory comments to help developers understand or modify it:

import base64
from io import BytesIO
from PIL import Image
import numpy as np

def detect_swirly_edge_artifact(base64_image: str, threshold: float = 0.70) -> bool:
    """
    Detects the presence of a high-contrast, swirly-edge artifact in an image.

    Args:
        base64_image (str):
            The image encoded in base64. Supports PNG, JPG, WebP.
            A data-URL header is permitted and will be stripped automatically.

        threshold (float, optional):
            The detection threshold value between 0 and 1.
            Higher thresholds decrease false positives but might miss subtle artifacts.
            Default is 0.70.

    Returns:
        bool: 
            True if the artifact is detected, False otherwise.
    """

    # ----- Step 1: Convert base64 string to PIL image -----
    if ';base64,' in base64_image:
        base64_image = base64_image.split(';base64,')[-1]

    image_bytes = base64.b64decode(base64_image)
    image = Image.open(BytesIO(image_bytes)).convert("RGB")

    # ----- Step 2: Convert image to NumPy array (RGB channels) -----
    img_array = np.asarray(image, dtype=np.float32)

    # ----- Step 3: Calculate luminance -----
    luminance = 0.299 * img_array[..., 0] + \
                0.587 * img_array[..., 1] + \
                0.114 * img_array[..., 2]

    # ----- Step 4: Calculate edge density -----
    # Measure the gradient magnitude of luminance to identify edges
    gradient_x = np.abs(np.diff(luminance, axis=1, prepend=luminance[:, :1]))
    gradient_y = np.abs(np.diff(luminance, axis=0, prepend=luminance[:1, :]))
    gradient_magnitude = np.hypot(gradient_x, gradient_y)

    # Define a pixel as 'edge' if gradient magnitude exceeds this threshold
    edge_detection_threshold = 60.0
    edge_pixel_ratio = np.mean(gradient_magnitude > edge_detection_threshold)

    # Normalize edge density to 0-1 (typical artifacts have very high edge density)
    normalized_edge_ratio = min(edge_pixel_ratio / 0.40, 1.0)

    # ----- Step 5: Calculate sharpness using Tenengrad method -----
    # Tenengrad: a common autofocus measure of high-frequency detail
    gx = np.zeros_like(luminance)
    gy = np.zeros_like(luminance)

    gx[1:-1, 1:-1] = (luminance[1:-1, 2:] - luminance[1:-1, :-2]) * 2 + \
                     (luminance[:-2, 2:] - luminance[:-2, :-2]) + \
                     (luminance[2:, 2:] - luminance[2:, :-2])

    gy[1:-1, 1:-1] = (luminance[2:, 1:-1] - luminance[:-2, 1:-1]) * 2 + \
                     (luminance[2:, 2:] - luminance[:-2, 2:]) + \
                     (luminance[2:, :-2] - luminance[:-2, :-2])

    # Tenengrad sharpness metric (mean squared gradient)
    tenengrad_sharpness = np.mean(gx ** 2 + gy ** 2)

    # Normalizing sharpness (artifact images have excessively high sharpness)
    normalized_sharpness = min(tenengrad_sharpness / 150_000.0, 1.0)

    # ----- Step 6: Calculate mean inverse saturation (artifact images have low saturation) -----
    img_normalized = img_array / 255.0
    max_rgb = np.max(img_normalized, axis=-1)
    min_rgb = np.min(img_normalized, axis=-1)

    # Avoid division by zero for black pixels
    with np.errstate(divide='ignore', invalid='ignore'):
        saturation = np.where(max_rgb == 0, 0, (max_rgb - min_rgb) / max_rgb)

    inverse_mean_saturation = 1.0 - np.mean(saturation)

    # ----- Step 7: Calculate proportion of extreme grayscale pixels -----
    # (pixels nearly black or nearly white indicate the artifact)
    near_black_threshold = 30
    near_white_threshold = 225
    extreme_pixels = np.logical_or(
        luminance < near_black_threshold,
        luminance > near_white_threshold
    )
    extreme_pixel_ratio = np.mean(extreme_pixels)
    normalized_extreme_ratio = min(extreme_pixel_ratio / 0.50, 1.0)

    # ----- Step 8: Calculate inverse colorfulness (artifacts lack colors) -----
    red, green, blue = img_array[..., 0], img_array[..., 1], img_array[..., 2]
    rg_diff = np.abs(red - green)
    yb_diff = np.abs(0.5 * (red + green) - blue)

    # Colorfulness metric based on variance and mean differences between color channels
    colorfulness = np.sqrt(np.var(rg_diff) + np.var(yb_diff)) + \
                   0.3 * np.sqrt(np.mean(rg_diff) ** 2 + np.mean(yb_diff) ** 2)

    # Normalize inverse colorfulness (artifacts have lower colorfulness)
    inverse_colorfulness = 1.0 - min(colorfulness / 40.0, 1.0)

    # ----- Step 9: Combine features into composite anomaly score -----
    anomaly_score = (
        0.25 * normalized_edge_ratio +        # Edge density
        0.45 * normalized_sharpness +         # Sharpness
        0.12 * inverse_mean_saturation +      # Low saturation
        0.10 * normalized_extreme_ratio +     # Extreme brightness/darkness
        0.08 * inverse_colorfulness           # Low colorfulness
    )

    # ----- Step 10: Flag image if anomaly_score exceeds threshold -----
    return anomaly_score >= threshold

You might also have the function return the anomaly_score instead of just a boolean flag.

How to use the refactored function:

# Example usage, use a file instead of your API return base64:
with open('path_to_image.png', 'rb') as file:
    base64_str = base64.b64encode(file.read()).decode('ascii')

# Detect artifact presence with function taking base64 image files
if detect_swirly_edge_artifact(base64_str, threshold=0.70):
    print("Artifact detected in image.")
else:
    print("No artifact detected in image.")

Developer Notes:

  • Adjustable Threshold:

    • The default threshold is 0.70.
    • Lower threshold values increase sensitivity but may introduce false positives.
    • Higher threshold values improve precision, reducing false positives.
  • Modifiable Parameters:

    • Internal parameters (e.g., edge_detection_threshold, near_black_threshold, near_white_threshold, and normalization values) are easily adjustable to refine detection based on your specific dataset.
  • External Dependencies:

    • Relies only on widely-used libraries (numpy, Pillow).

Advanced code idea to automate

Log into your SMTP email server, send support@openai every bad image request id and a MIME file attachment, and ask for an org ID credit until they fix it.

Very cool, if we are able to detect if this is happening, do you think we are also able to apply an edit to the image to undo this effect?

As I described before, to “undo” one of the previous reply’s damaged images and get something passable, I did use another pass (of ChatGPT). And got the same symptom also, of good previews that were looking good until a final deliverable with the same symptom again. I did get success also, but I don’t expect the successful “repair” to look much like the input that was only “remove the bushes”.

It seems to be a random fault, since it is happening on API and ChatGPT, happening whether the input image is the original output base64 or a rewritten JPG or an image further modified by scripting. The only thing you could blame on being a previous AI output is the aspect ratio and size.

The only thing not completely documented is if you ever get this on API edits without using the new fidelity parameter.

I would not attempt to fix an image damaged that has barely a hint of the original input images. I would detect, and retry the call, with or without fidelity or at a different quality parameter if acceptable for the application. If that fails also, deliver the retry and report that you detected that it contains randomly-occurring issues that are under investigation (or continue attempts, depending on how much you want to pay in retries).

There may be prompting to make image intermediaries and content that are less “triggering”, like requesting “soft focus”, or downsizing the input images. Or it could just be 5% of any image.

I did not mean to repair, I was just saying we might be able to transform the initial output image (the first one created by AI) so that the second round does not include this artifact. Something like a denoiser might distort the image enough where it does not have this effect on the second run.

Beat this: another final pass before deliverable that could be the culprit in multi-turn generation, in a library that would inherently take the AI output and attempt to invisibly damage the image data…or enforce controls over multiple generations of edits until it can’t.


Interesting, I think that very likely it is related to the processing engine applying the same “watermark” to the image in multiple turns.

If that’s the case, certainly OpenAI can detect that the image already has this and just not apply this filter?

Yes, run multiple threads and do two or three and switch. You also have to make sure you lay on the right persona for each one. If you do not keep that thread active for more than around 12 hours, you lay on the persona again. Memory for long stretches gets messed up. You can also sync threads to the same AI.

That’s not really what is happening for me. In my case these “edits” are completely unrelated to any chats at all. Even when completely seperated, these artifacts still show up.

@nls @_j I have been researching this and this is what I have found:

Steps To Reproduce Artifacts via API:

(1) The original photo-realistic image to modify must not have been created by OpenAI (e.g. camera photos).

(2) Modify the original image (shown in Step 1) however you wish. Note: You may have to handle the aspect ratio issue - you can add the following to your prompt:

Caution: You must compute the aspect ratio for the specified size of the new image and fit the provided image into the new image accordingly."

(3) Try to modify the image created in Step 2. This will result in artifacts.

I think this is a bug that needs to be fixed.

Steps To Not Reproduce Artifacts via API:

(A) The original photo-realistic image to modify must have been created by OpenAI (e.g. DALL-E 3, Image-1).

(B) Modify the original image (shown in Step A) however you wish.

(C) Try to modify the image created in Step B. This will result in no artifacts.

Why is there no option to view “temporarily hidden” messages? I want to see it.

“Hidden” is the status of forum posts flagged by the community until mods resolve the report (or leave it merely hidden without account strikes).

It is reasonable to assume that flagged and reported content should not be made available to be read on an internet forum.

ok nvm i guess i was a little aggressive in saying it was a solution hahaha. Edited it thought, and it may work for some like it worked for me.

I’m getting this graininess too just as the op described. I read the thread but couldn’t quite understand. Is there a reliable workaround? Starting a new session in gpt and reuploading the image does not solve the issue for me. I still get the graininess.

Yes this is still a problem and, in my opinion, a bug because the issue can be reproduced.

Is there a reliable around?

It’s hit or miss - depends on the prompt.

Orginal:

Edit:

Note: I’m using the gpt image-1 api edits endpoint and not ChatGPT.