GIMP — Python Manipulation Guide

SkillMedia

How to programmatically create, modify, and verify images using Python Pillow, NumPy, OpenCV, and scikit-image. For setup-gen and reward-gen agents.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the GIMP — Python Manipulation Guide skill

What this skill tells your AI

The instructions your AI receives, as published by xlang-ai/cua-gym in .claude/skills/gimp/SKILL.md and read by ahel’s review.

This skill teaches setup-gen (create/modify images) and reward-gen (read/verify image properties) how to work with image files using pure Python code.

  • Libraries: Pillow, numpy, opencv-python, scikit-image
  • Install: pip3 install Pillow numpy opencv-python scikit-image
  • File formats: .png, .jpg, .bmp, .xcf (GIMP native, read-only via special tools)

0. GUI Startup on VM (for setup-gen)

After generating image assets, setup-gen should open GIMP with the target initial file for the GUI agent.

CRITICAL VM LIMIT: GUI launches must set DISPLAY=:0.

import os
import shlex
import subprocess
import time

def launch_gui(command: str, delay_sec: float = 1.0):
    env = os.environ.copy()
    env["DISPLAY"] = ":0"
    subprocess.Popen(
        shlex.split(command),
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        env=env,
    )
    time.sleep(delay_sec)

# Open target image in GIMP
launch_gui('gimp "/home/user/<task_id>_initial.png"', delay_sec=2.0)

Guidelines:

  • Open initial artifact(s), never golden artifact(s).
  • Use non-blocking launch (Popen) and short delays.

0.5. Media Asset Library (setup-gen)

Instead of generating synthetic images (color blocks, testsrc patterns), use the real photo library at assets/media/gimp/. These are high-resolution photos from Pexels (free commercial use).

Picking Assets from Manifest

import json, os, random

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # project root
MANIFEST = os.path.join(ROOT, "assets", "media", "manifest.json")

def pick_asset(domain: str, category: str = None, tags: list = None) -> dict:
    """Pick a random asset matching domain/category/tags from manifest."""
    with open(MANIFEST) as f:
        manifest = json.load(f)
    candidates = [a for a in manifest["assets"] if a["domain"] == domain]
    if category:
        candidates = [a for a in candidates if a["category"] == category]
    if tags:
        candidates = [a for a in candidates if any(t in a.get("tags", []) for t in tags)]
    return random.choice(candidates) if candidates else None

# Pick a landscape photo for a brightness adjustment task
asset = pick_asset("gimp", "photos", tags=["landscape", "nature"])
local_path = os.path.join(ROOT, asset["path"])  # e.g., assets/media/gimp/photos/landscape_mountain_001.jpg

Uploading to VM

# Upload a photo to the VM before running initial_setup.py
python3 scripts/env_cli.py -c "<workdir>/env_config_initial.json" upload \
    "assets/media/gimp/photos/landscape_mountain_001.jpg" "/home/user/<task_id>.jpg"
python3 scripts/env_cli.py -c "<workdir>/env_config_golden.json" upload \
    "assets/media/gimp/photos/landscape_mountain_001.jpg" "/home/user/<task_id>.jpg"

Available Categories

CategoryCountDescriptionGood for
photos~50Real high-res photos (landscape, portrait, food, architecture, etc.)Brightness, contrast, crop, resize, rotate, color grading
graphics~10Geometric patterns, gradients, abstract artColor mode conversion, palette, effects
icons~10Logo/icon style imagesTransparency, compositing, background removal

Pattern: Upload + Reference Copy

# Setup-gen should upload the asset to BOTH VMs, then initial_setup.py saves the reference copy
python3 scripts/env_cli.py -c "<workdir>/env_config_initial.json" upload "assets/media/gimp/photos/xxx.jpg" "/home/user/<task_id>.jpg"
python3 scripts/env_cli.py -c "<workdir>/env_config_golden.json"  upload "assets/media/gimp/photos/xxx.jpg" "/home/user/<task_id>.jpg"

Then in initial_setup.py:

import shutil
shutil.copy(f'{WORKDIR}/{TASK_ID}.jpg', f'{WORKDIR}/{TASK_ID}_initial_reference.jpg')
# ... launch GIMP with the file

1. Creating & Modifying Images (setup-gen)

Basic Image Creation

from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageChops, ImageStat, ImageEnhance
from PIL.Image import Resampling
import numpy as np
import shutil, os

# Create blank image
img = Image.new("RGB", (800, 600), color=(255, 255, 255))
img.save("/home/user/Desktop/blank.png")

# Create RGBA image with transparency
img = Image.new("RGBA", (800, 600), color=(0, 0, 0, 0))
img.save("/home/user/Desktop/transparent.png")

Drawing Shapes and Text

draw = ImageDraw.Draw(img)
# Rectangle
draw.rectangle([100, 100, 300, 200], fill="red", outline="black", width=2)
# Circle / ellipse
draw.ellipse([400, 100, 550, 250], fill="blue")
# Triangle (polygon)
draw.polygon([(400, 300), (300, 500), (500, 500)], fill="yellow", outline="black")
# Line
draw.line([(0, 0), (800, 600)], fill="green", width=3)
# Text
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 36)
draw.text((100, 50), "Hello World", fill="black", font=font)
img.save("/home/user/Desktop/shapes.png")

Image Transformations

img = Image.open("/home/user/Desktop/photo.png")

# Resize (maintain aspect ratio)
target_height = 512
ratio = target_height / img.height
new_size = (int(img.width * ratio), target_height)
img_resized = img.resize(new_size, Resampling.LANCZOS)

# Crop
img_cropped = img.crop((left, top, right, bottom))  # box = (left, upper, right, lower)

# Rotate
img_rotated = img.rotate(90, expand=True)  # expand=True to fit full rotated image

# Mirror / flip
img_mirror = img.transpose(Image.FLIP_LEFT_RIGHT)     # horizontal mirror
img_flip = img.transpose(Image.FLIP_TOP_BOTTOM)        # vertical flip

# Convert color mode
img_gray = img.convert("L")         # grayscale
img_rgb = img.convert("RGB")        # RGB (drops alpha)
img_rgba = img.convert("RGBA")      # add alpha channel
img_palette = img.convert("P")      # indexed/palette mode
img_hsv = img.convert("HSV")        # hue-saturation-value

Brightness, Contrast, Saturation Adjustments

# Brightness
enhancer = ImageEnhance.Brightness(img)
img_darker = enhancer.enhance(0.7)    # < 1 = darker, > 1 = brighter

# Contrast
enhancer = ImageEnhance.Contrast(img)
img_contrast = enhancer.enhance(1.5)  # > 1 = more contrast

# Saturation / Color
enhancer = ImageEnhance.Color(img)
img_vivid = enhancer.enhance(1.5)     # > 1 = more saturated

# Sharpness
enhancer = ImageEnhance.Sharpness(img)
img_sharp = enhancer.enhance(2.0)     # > 1 = sharper

Transparency & Background Removal

# Make white background transparent
img = img.convert("RGBA")
data = np.array(img)
# Replace white pixels with transparent
white_mask = (data[:, :, 0] > 240) & (data[:, :, 1] > 240) & (data[:, :, 2] > 240)
data[white_mask, 3] = 0
img = Image.fromarray(data)

# Fill background layer with a color (keeping object intact)
bg = Image.new("RGB", img.size, (0, 255, 0))  # green background
if img.mode == "RGBA":
    bg.paste(img, mask=img.split()[3])  # paste using alpha as mask

Filters

img_blur = img.filter(ImageFilter.GaussianBlur(radius=5))
img_sharp = img.filter(ImageFilter.SHARPEN)
img_edges = img.filter(ImageFilter.FIND_EDGES)
img_emboss = img.filter(ImageFilter.EMBOSS)

Golden File Pattern

# Always copy-then-modify for golden files
shutil.copy("/home/user/Desktop/original.png", "/home/user/Desktop/golden.png")
img = Image.open("/home/user/Desktop/golden.png")
img = img.transpose(Image.FLIP_LEFT_RIGHT)  # example transformation
img.save("/home/user/Desktop/golden.png")

Compositing Multiple Images

bg = Image.open("/home/user/Desktop/background.png").convert("RGBA")
fg = Image.open("/home/user/Desktop/foreground.png").convert("RGBA")
# Paste foreground onto background at position
bg.paste(fg, (100, 100), mask=fg)  # use alpha as mask
bg.save("/home/user/Desktop/composite.png")

2. Reading & Verifying Images (reward-gen)

Structure Similarity (SSIM)

from skimage.metrics import structural_similarity as ssim

def check_ssim(src_path: str, tgt_path: str, threshold: float = 0.9) -> bool:
    """Compare two same-sized images by SSIM. Returns True if similar."""
    img1 = Image.open(src_path).convert("RGB")
    img2 = Image.open(tgt_path).convert("RGB")
    if img1.size != img2.size:
        return False
    arr1, arr2 = np.array(img1), np.array(img2)
    min_dim = min(arr1.shape[0], arr1.shape[1])
    win_size = min(7, min_dim if min_dim % 2 == 1 else min_dim - 1)
    if win_size < 1:
        return False
    try:
        score = ssim(arr1, arr2, win_size=win_size, channel_axis=2)
    except TypeError:
        score = ssim(arr1, arr2, win_size=win_size, multichannel=True)
    return score >= threshold

def check_ssim_resized(src_path: str, tgt_path: str, threshold: float = 0.9) -> bool:
    """SSIM with auto-resize and transparency handling."""
    img1 = Image.open(src_path)
    img2 = Image.open(tgt_path)
    # Handle transparency: crop to content bounding box
    if img1.mode in ("RGBA", "LA"):
        alpha = img1.split()[-1]
        bbox = alpha.getbbox()
        if bbox:
            img1 = img1.crop(bbox)
    img1 = img1.convert("RGB").resize(img2.size if img2.mode == "RGB" else img2.convert("RGB").size, Resampling.LANCZOS)
    img2 = img2.convert("RGB")
    arr1, arr2 = np.array(img1), np.array(img2)
    try:
        score = ssim(arr1, arr2, win_size=7, channel_axis=2)
    except TypeError:
        score = ssim(arr1, arr2, win_size=7, multichannel=True)
    return score >= threshold

Pixel-Perfect Comparison

def compare_exact(src_path: str, tgt_path: str) -> bool:
    """Binary comparison: True only if all pixels match."""
    img1 = Image.open(src_path)
    img2 = Image.open(tgt_path)
    if img1.size != img2.size:
        img1 = img1.resize(img2.size, Resampling.LANCZOS)
    if img1.mode != img2.mode:
        img1 = img1.convert(img2.mode)
    diff = ImageChops.difference(img1, img2)
    return diff.getbbox() is None

Brightness Verification

def calculate_brightness(img: Image.Image) -> float:
    gray = img.convert("L")
    return ImageStat.Stat(gray).mean[0]

def verify_brightness_decreased(src_path: str, tgt_path: str) -> bool:
    """Verify src is darker than tgt AND structure preserved."""
    img_src = Image.open(src_path)
    img_tgt = Image.open(tgt_path)
    if calculate_brightness(img_src) >= calculate_brightness(img_tgt):
        return False
    # Normalize both to same brightness, then compare structure
    def normalize(img, target=128):
        factor = target / max(calculate_brightness(img), 1)
        return img.point(lambda x: min(255, max(0, int(x * factor))))
    mse = np.mean((np.array(normalize(img_src), dtype=np.float32) / 255 -
                    np.array(normalize(img_tgt), dtype=np.float32) / 255) ** 2)
    return mse < 0.03

Contrast Verification

def calculate_contrast(img: Image.Image) -> float:
    return float(np.std(np.asarray(img, dtype=np.float32)))

def verify_contrast_increased(src_path: str, tgt_path: str) -> bool:
    src, tgt = Image.open(src_path), Image.open(tgt_path)
    return calculate_contrast(src) > calculate_contrast(tgt) and check_ssim(src_path, tgt_path, threshold=0.65)

Saturation Verification

def verify_saturation_increased(src_path: str, tgt_path: str) -> bool:
    src_hsv = Image.open(src_path).convert("HSV")
    tgt_hsv = Image.open(tgt_path).convert("HSV")
    src_sat = np.mean(np.array(src_hsv.split()[1]))
    tgt_sat = np.mean(np.array(tgt_hsv.split()[1]))
    if src_sat <= tgt_sat:
        return False  # saturation did not increase
    # H and V channels must match
    h1, _, v1 = src_hsv.split()
    h2, _, v2 = tgt_hsv.split()
    return check_ssim_channel(h1, h2) and check_ssim_channel(v1, v2)

def check_ssim_channel(ch1, ch2, threshold=0.9):
    arr1, arr2 = np.array(ch1.convert("RGB")), np.array(ch2.convert("RGB"))
    if arr1.shape != arr2.shape:
        return False
    try:
        return ssim(arr1, arr2, win_size=7, channel_axis=2) >= threshold
    except TypeError:
        return ssim(arr1, arr2, win_size=7, multichannel=True) >= threshold

Image Size Verification

def verify_image_size(src_path: str, width: int = None, height: int = None,
                      ignore_transparent: bool = False) -> bool:
    img = Image.open(src_path)
    if ignore_transparent and img.mode in ("RGBA", "LA"):
        alpha = img.split()[-1]
        bbox = alpha.getbbox()
        if bbox is None:
            return False
        w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
    else:
        w, h = img.size
    if width is not None and w != width:
        return False
    if height is not None and h != height:
        return False
    return True

Mirror Verification

def verify_mirror(src_path: str, tgt_path: str) -> bool:
    src = Image.open(src_path)
    tgt = Image.open(tgt_path)
    flipped = src.transpose(Image.FLIP_LEFT_RIGHT)
    arr1, arr2 = np.array(flipped.convert("RGB")), np.array(tgt.convert("RGB"))
    if arr1.shape != arr2.shape:
        return False
    try:
        return ssim(arr1, arr2, win_size=7, channel_axis=2) >= 0.99
    except TypeError:
        return ssim(arr1, arr2, win_size=7, multichannel=True) >= 0.99

Palette Mode Verification

def verify_palette_mode(src_path: str, tgt_path: str) -> bool:
    """Verify image is palette-based and structure matches."""
    img = Image.open(src_path)
    if img.mode != "P":
        return False
    return check_ssim(src_path, tgt_path, threshold=0.9)  # compare after RGB conversion

Green Background Verification

def verify_green_background(src_path: str, tgt_path: str) -> bool:
    """All non-black target pixels must have green source pixels (g > r and g > b)."""
    src = np.array(Image.open(src_path))
    tgt = np.array(Image.open(tgt_path))
    # Vectorized: find non-black pixels in target
    mask = np.any(tgt[:, :, :3] != 0, axis=2)
    if not np.any(mask):
        return True
    r, g, b = src[mask, 0], src[mask, 1], src[mask, 2]
    return bool(np.all(g > r) and np.all(g > b))

Shape Position Verification

def verify_centered(tgt_path: str, tolerance: float = 0.05) -> bool:
    """Verify a colored shape is centered within tolerance of image center."""
    img = np.array(Image.open(tgt_path))
    unique_colors, counts = np.unique(img.reshape(-1, img.shape[2]), axis=0, return_counts=True)
    sorted_colors = unique_colors[np.argsort(counts)]
    shape_color = sorted_colors[1]  # second most common = shape (not background)
    mask = np.all(img == shape_color, axis=2)
    coords = np.argwhere(mask)
    centroid = coords.mean(axis=0)
    center = np.array(img.shape[:2]) / 2
    return bool(np.all(np.abs(centroid - center) < tolerance * np.array(img.shape[:2])))

Text Position Verification

def verify_text_on_left(src_path: str, width_threshold: float = 0.05) -> bool:
    """Verify dark text starts within the left edge of the image."""
    gray = np.array(Image.open(src_path).convert("L"))
    h, w = gray.shape
    dark_mask = gray < 128
    if not np.any(dark_mask):
        return False
    # Find the leftmost dark pixel across all rows
    cols = np.where(dark_mask.any(axis=0))[0]
    left_most = cols[0] if len(cols) > 0 else w
    return left_most < w * width_threshold

Sharpness Verification

import cv2

def verify_sharper(src_path: str, tgt_path: str) -> bool:
    src = cv2.imread(src_path, cv2.IMREAD_GRAYSCALE)
    tgt = cv2.imread(tgt_path, cv2.IMREAD_GRAYSCALE)
    return float(np.var(cv2.Laplacian(src, cv2.CV_64F))) > float(np.var(cv2.Laplacian(tgt, cv2.CV_64F)))

File Existence & Size Verification

def verify_file_exists_and_similar(src_path: str, tgt_path: str) -> bool:
    if not os.path.isfile(src_path):
        return False
    return check_ssim(src_path, tgt_path)

def verify_file_size(src_path: str, max_bytes: int) -> bool:
    return os.path.isfile(src_path) and os.path.getsize(src_path) < max_bytes

GIMP Config Verification

def verify_gimp_config(config_path: str, key, value: str) -> bool:
    """Check GIMP gimprc config file for key-value pair.
    key can be str or list of str (for multi-word keys)."""
    with open(config_path, "r") as f:
        for line in f:
            if line.startswith("#") or line.strip() == "":
                continue
            items = line.strip().lstrip("(").rstrip(")").split()
            if isinstance(key, str):
                if items[0] == key and items[-1] == value:
                    return True
            elif isinstance(key, list) and len(key) == 2:
                if items[0] == key[0] and items[1] == key[1] and items[-1] == value:
                    return True
    return False

3. XCF (GIMP Native Format) Reading

GIMP's native .xcf format stores layers, channels, and paths. Pillow cannot read XCF. Use these approaches:

Reading XCF with python-xcftools

import subprocess, json, re

def get_xcf_info(xcf_path: str) -> dict:
    """Extract XCF metadata: dimensions, layers, color mode."""
    result = subprocess.run(["xcfinfo", xcf_path], capture_output=True, text=True)
    info = {"layers": [], "width": 0, "height": 0}
    for line in result.stdout.strip().split("\n"):
        # First line: "Version X, WxH RGB color, N layers, ..."
        if line.startswith("Version"):
            m = re.search(r'(\d+)x(\d+)\s+(\w+)', line)
            if m:
                info["width"], info["height"] = int(m.group(1)), int(m.group(2))
                info["color_mode"] = m.group(3)
        # Layer lines: "+ WxH+X+Y RGB-alpha Normal name"
        elif line.startswith("+") or line.startswith("-"):
            parts = line.strip().split()
            visible = parts[0] == "+"
            # Layer name is last quoted or unquoted token
            name = " ".join(parts[4:]) if len(parts) > 4 else ""
            info["layers"].append({"name": name, "visible": visible})
    return info

def get_xcf_layer_count(xcf_path: str) -> int:
    info = get_xcf_info(xcf_path)
    return len(info["layers"])

def verify_xcf_has_layer(xcf_path: str, layer_name: str) -> bool:
    info = get_xcf_info(xcf_path)
    return any(layer_name.lower() in l["name"].lower() for l in info["layers"])

Flattening XCF to PNG for Comparison

def xcf_to_png(xcf_path: str, png_path: str) -> bool:
    """Flatten XCF to PNG using xcf2png (from xcftools package)."""
    result = subprocess.run(["xcf2png", xcf_path, "-o", png_path], capture_output=True)
    return result.returncode == 0

def xcf_extract_layer(xcf_path: str, layer_name: str, png_path: str) -> bool:
    """Extract a single layer from XCF to PNG."""
    result = subprocess.run(
        ["xcf2png", xcf_path, layer_name, "-o", png_path],
        capture_output=True
    )
    return result.returncode == 0

# Install on VM: sudo apt install xcftools

4. GIMP Script-Fu Batch Mode

For tasks that require GIMP-specific operations (filters, layer manipulation), use headless batch mode.

def gimp_batch(script_fu: str, timeout: int = 30) -> str:
    """Run a Script-Fu command in GIMP batch mode (headless)."""
    result = subprocess.run(
        ["gimp", "-i", "-b", script_fu, "-b", "(gimp-quit 0)"],
        capture_output=True, text=True, timeout=timeout,
        env={**os.environ, "DISPLAY": ":0"}
    )
    return result.stdout + result.stderr

# Flatten an XCF and export as PNG
gimp_batch('(let* ((image (car (gimp-file-load RUN-NONINTERACTIVE "/home/user/file.xcf" "file.xcf"))) '
           '(drawable (car (gimp-image-flatten image)))) '
           '(file-png-save RUN-NONINTERACTIVE image drawable "/home/user/flat.png" "flat.png" 0 9 1 1 1 1 1))')

# Apply Gaussian blur and export
gimp_batch('(let* ((image (car (gimp-file-load RUN-NONINTERACTIVE "/home/user/in.png" "in.png"))) '
           '(drawable (car (gimp-image-flatten image)))) '
           '(plug-in-gauss RUN-NONINTERACTIVE image drawable 10 10 0) '
           '(gimp-file-overwrite RUN-NONINTERACTIVE image drawable "/home/user/out.png" "out.png"))')

# Get image properties (layer count, dimensions)
gimp_batch('(let* ((image (car (gimp-file-load RUN-NONINTERACTIVE "/home/user/file.xcf" "file.xcf")))) '
           '(gimp-message (number->string (car (gimp-image-get-active-layer image)))) '
           '(gimp-message (number->string (car (gimp-image-width image)))))')

Guidelines:

  • -i = no GUI, -b = batch command
  • Always end with (gimp-quit 0) or GIMP hangs
  • Script-Fu uses Scheme syntax with car to unwrap return lists
  • Set timeout — GIMP batch can hang on missing fonts or corrupt files

5. Color Space & Color Verification

Color Temperature / White Balance Verification

def calculate_color_temperature_shift(src_path: str, tgt_path: str) -> tuple:
    """Calculate average RGB shift between two images. Returns (dr, dg, db)."""
    src = np.array(Image.open(src_path).convert("RGB"), dtype=np.float32)
    tgt = np.array(Image.open(tgt_path).convert("RGB"), dtype=np.float32)
    if src.shape != tgt.shape:
        tgt = np.array(Image.open(tgt_path).convert("RGB").resize(
            Image.open(src_path).size, Resampling.LANCZOS), dtype=np.float32)
    diff = np.mean(src - tgt, axis=(0, 1))
    return tuple(diff)

def verify_warmer_tone(src_path: str, tgt_path: str) -> bool:
    """Verify src has warmer tone than tgt (more red/yellow, less blue)."""
    dr, dg, db = calculate_color_temperature_shift(src_path, tgt_path)
    return dr > 2 and db < -2  # red increased, blue decreased

def verify_cooler_tone(src_path: str, tgt_path: str) -> bool:
    """Verify src has cooler tone than tgt (more blue, less red)."""
    dr, dg, db = calculate_color_temperature_shift(src_path, tgt_path)
    return dr < -2 and db > 2

Histogram-Based Color Distribution

def get_color_histogram(img_path: str, bins: int = 64) -> np.ndarray:
    """Get normalized RGB histogram."""
    img = np.array(Image.open(img_path).convert("RGB"))
    hist_r = np.histogram(img[:,:,0], bins=bins, range=(0,256))[0]
    hist_g = np.histogram(img[:,:,1], bins=bins, range=(0,256))[0]
    hist_b = np.histogram(img[:,:,2], bins=bins, range=(0,256))[0]
    hist = np.concatenate([hist_r, hist_g, hist_b]).astype(np.float32)
    return hist / hist.sum()

def verify_histogram_similar(src_path: str, tgt_path: str, threshold: float = 0.85) -> bool:
    """Compare color distributions via histogram correlation."""
    h1 = get_color_histogram(src_path)
    h2 = get_color_histogram(tgt_path)
    correlation = float(np.corrcoef(h1, h2)[0, 1])
    return correlation >= threshold

def verify_grayscale(img_path: str) -> bool:
    """Verify image is grayscale (R == G == B for all pixels, or mode is L)."""
    img = Image.open(img_path)
    if img.mode == "L":
        return True
    if img.mode in ("RGB", "RGBA"):
        arr = np.array(img.convert("RGB"))
        return bool(np.all(arr[:,:,0] == arr[:,:,1]) and np.all(arr[:,:,1] == arr[:,:,2]))
    return False

def verify_inverted(src_path: str, tgt_path: str, threshold: float = 0.95) -> bool:
    """Verify src is the color-inverted version of tgt."""
    src = np.array(Image.open(src_path).convert("RGB"), dtype=np.float32)
    tgt = np.array(Image.open(tgt_path).convert("RGB"), dtype=np.float32)
    if src.shape != tgt.shape:
        return False
    inverted_tgt = 255.0 - tgt
    mse = np.mean((src - inverted_tgt) ** 2)
    return mse < (1.0 - threshold) * 255 * 255

6. Gradual Scoring Pattern (reward-gen)

def compute_reward(src_path: str, golden_path: str, task_type: str = "transform",
                   checks: list = None) -> float:
    """Compute a 0.0-1.0 reward score with multiple verification dimensions.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
197
Forks
18
Last commit
Aug 2026

ahel review

  • K1binfo
    installs-packages

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
gimp
Source
github.com/xlang-ai/cua-gym