File size: 6,719 Bytes
7f7be37
 
eb818e4
7f7be37
9e1be05
 
 
 
 
7f7be37
01445c3
7f7be37
eb818e4
 
 
 
01445c3
 
 
 
eb818e4
 
 
01445c3
 
eb818e4
 
01445c3
eb818e4
 
01445c3
 
eb818e4
01445c3
 
 
 
eb818e4
 
 
 
 
 
 
7f7be37
 
 
01445c3
 
 
7f7be37
 
 
 
 
 
 
 
 
01445c3
7f7be37
 
01445c3
 
 
 
 
 
 
 
 
 
 
 
 
7f7be37
 
 
 
 
 
 
f49268a
9e1be05
ce906ec
9e1be05
7f7be37
9e1be05
ce906ec
7f7be37
ce906ec
f49268a
ce906ec
7f7be37
9e1be05
 
01445c3
9e1be05
 
 
f49268a
9e1be05
 
 
 
eb818e4
9e1be05
ce906ec
01445c3
ce906ec
 
 
 
 
 
 
7f7be37
9e1be05
ce906ec
9e1be05
eb818e4
7f7be37
ce906ec
eb818e4
ce906ec
 
 
 
 
9e1be05
7f7be37
 
 
 
 
 
 
 
9e1be05
7f7be37
 
 
 
 
 
 
ce906ec
 
9e1be05
 
7f7be37
 
 
f49268a
ce906ec
f49268a
9e1be05
f49268a
ce906ec
 
01445c3
7f7be37
 
eb818e4
01445c3
7f7be37
eb818e4
 
7f7be37
9e1be05
 
f49268a
ce906ec
9e1be05
f49268a
9e1be05
f49268a
ce906ec
f49268a
 
 
ce906ec
 
 
f49268a
ce906ec
f49268a
 
 
ce906ec
f49268a
9e1be05
 
ce906ec
 
9e1be05
 
ce906ec
 
 
9e1be05
 
 
ce906ec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
import os
import tempfile
from pathlib import Path

import gradio as gr
import cv2
import numpy as np
from PIL import Image, ImageCms

# =========================================================
# Color profile assets (Gradio Space safe)
# =========================================================
BASE_DIR = Path(__file__).resolve().parent
ICC_DIR = BASE_DIR / "icc"

ADOBE_ICC_PATH = ICC_DIR / "AdobeRGB1998.icc"

# sRGB profile is always available via Pillow (no file needed)
SRGB_PROFILE = ImageCms.createProfile("sRGB")
SRGB_ICC_BYTES = ImageCms.ImageCmsProfile(SRGB_PROFILE).tobytes()

def safe_open_profile(path: Path):
    """
    Safely open ICC profile.
    AdobeRGB1998 can legitimately be small (e.g., ~560 bytes), so do NOT size-gate.
    """
    try:
        if not path.exists():
            print(f"⚠️ ICC missing: {path}")
            return None
        if not path.is_file():
            print(f"⚠️ ICC is not a file: {path}")
            return None

        size = path.stat().st_size
        print(f"ℹ️ ICC found: {path} ({size} bytes)")

        prof = ImageCms.getOpenProfile(str(path))
        return prof
    except Exception as e:
        print(f"⚠️ ICC open failed: {path}{e}")
        return None

ADOBE_PROFILE = safe_open_profile(ADOBE_ICC_PATH)

def convert_and_tag(img: Image.Image, target_color_space: str):
    """
    Converts pixel values to the selected target profile AND returns ICC bytes
    for embedding into the exported file.
    Incoming images are treated as sRGB by default (practical for AI/web images).
    """
    if img.mode != "RGB":
        img = img.convert("RGB")

    if target_color_space == "sRGB (Stock Standard)":
        return img, SRGB_ICC_BYTES

    if target_color_space == "Adobe RGB (1998)":
        if ADOBE_PROFILE is None:
            print("⚠️ AdobeRGB requested but profile not loaded; falling back to sRGB tagging.")
            return img, SRGB_ICC_BYTES

        try:
            xform = ImageCms.buildTransformFromOpenProfiles(
                SRGB_PROFILE,
                ADOBE_PROFILE,
                "RGB",
                "RGB",
                renderingIntent=0  # perceptual
            )
            out = ImageCms.applyTransform(img, xform)
            return out, ADOBE_PROFILE.tobytes()
        except Exception as e:
            print(f"⚠️ AdobeRGB transform failed; falling back to sRGB. Error: {e}")
            return img, SRGB_ICC_BYTES

    return img, SRGB_ICC_BYTES


# =========================================================
# 1) THE PROCESSING ENGINE
# =========================================================
def process_image(image, output_format, color_space, fix_lighting, add_grain):
    if image is None:
        return None, None

    # Convert PIL -> numpy
    img_array = np.array(image)

    # Stock safety: drop alpha if present
    if img_array.ndim == 3 and img_array.shape[2] == 4:
        img_array = cv2.cvtColor(img_array, cv2.COLOR_RGBA2RGB)

    # RGB -> BGR for OpenCV
    img_bgr = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)

    # A) LIGHTING FIX (CLAHE) - gentle
    if fix_lighting:
        lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)
        l, a, b = cv2.split(lab)
        clahe = cv2.createCLAHE(clipLimit=1.5, tileGridSize=(8, 8))
        l_fixed = clahe.apply(l)
        lab_fixed = cv2.merge((l_fixed, a, b))
        img_bgr = cv2.cvtColor(lab_fixed, cv2.COLOR_LAB2BGR)

    # B) TEXTURE FIX (Monochromatic Film Grain)
    if add_grain:
        h, w = img_bgr.shape[:2]
        # IMPORTANT: int16 prevents negative noise from wrapping under uint8
        noise = np.random.normal(loc=0.0, scale=2.5, size=(h, w)).astype(np.int16)
        img_i16 = img_bgr.astype(np.int16)
        img_i16[:, :, 0] += noise
        img_i16[:, :, 1] += noise
        img_i16[:, :, 2] += noise
        img_bgr = np.clip(img_i16, 0, 255).astype(np.uint8)

    # Convert back to PIL RGB
    img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
    final_pil = Image.fromarray(img_rgb).convert("RGB")

    # C) COLOR SPACE: convert + embed ICC
    final_pil, icc_bytes = convert_and_tag(final_pil, color_space)

    # D) EXPORT: create a real file for download
    suffix = ".jpg" if output_format == "JPEG" else ".png"
    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
    tmp_path = tmp.name
    tmp.close()

    if output_format == "JPEG":
        final_pil.save(
            tmp_path,
            format="JPEG",
            quality=100,
            subsampling=0,
            optimize=True,
            icc_profile=icc_bytes,
        )
    else:
        final_pil.save(
            tmp_path,
            format="PNG",
            compress_level=6,
            optimize=True,
            icc_profile=icc_bytes,
        )

    return final_pil, tmp_path


# =========================================================
# 2) THE UI (Gradio)
# =========================================================
css = """
#run-btn {background-color: #ff7c00 !important; color: white !important;}
"""

with gr.Blocks(title="StockFix AI", css=css) as app:
    gr.Markdown("## StockFix AI: De-Plasticizer")

    # Friendly status line
    if ADOBE_PROFILE is None:
        gr.Markdown(
            "⚠️ **Adobe RGB (1998) ICC not loaded.** "
            "Upload `icc/AdobeRGB1998.icc` to enable true Adobe RGB exports."
        )
    else:
        gr.Markdown("✅ Adobe RGB (1998) profile loaded.")

    with gr.Row():
        with gr.Column():
            input_img = gr.Image(type="pil", label="Input AI Image")

            with gr.Group():
                gr.Markdown("### 1. Fixes")
                chk_light = gr.Checkbox(label="Fix Flat Lighting (CLAHE)", value=True)
                chk_grain = gr.Checkbox(label="Add Film Grain (Monochromatic)", value=True)

            with gr.Group():
                gr.Markdown("### 2. Color Profile")
                radio_color = gr.Radio(
                    ["sRGB (Stock Standard)", "Adobe RGB (1998)"],
                    label="Target Color Space",
                    value="sRGB (Stock Standard)",
                )

            with gr.Group():
                gr.Markdown("### 3. Format")
                radio_fmt = gr.Radio(["JPEG", "PNG"], label="Output Format", value="JPEG")

            btn_run = gr.Button("Fix & Export", elem_id="run-btn")

        with gr.Column():
            output_img = gr.Image(label="Preview", type="pil")
            output_file = gr.File(label="Download (PNG/JPEG)")

    btn_run.click(
        fn=process_image,
        inputs=[input_img, radio_fmt, radio_color, chk_light, chk_grain],
        outputs=[output_img, output_file],
    )

if __name__ == "__main__":
    app.launch()