| | import gradio as gr |
| | import torch |
| | import os |
| | import tempfile |
| | import shutil |
| | from PIL import Image |
| | import numpy as np |
| | from pathlib import Path |
| | import sys |
| | import copy |
| | from options.test_options import TestOptions |
| | from data import create_dataset |
| | from models import create_model |
| | try: |
| | from best_ldr import compute_metrics_for_images, score_records |
| | except ImportError: |
| | |
| | raise ImportError("Could not import from best_ldr.py. Make sure the file is in the same directory as app.py.") |
| |
|
| | print("--- Initializing LDR-to-HDR Model (this may take a moment) ---") |
| |
|
| | |
| |
|
| | USAGE_GUIDELINES = """ |
| | ## 1. Quick Start Guide: Generating an HDR Image |
| | This tool uses a sophisticated AI model (CycleGAN) to translate the characteristics of a single, optimally selected Low Dynamic Range (LDR) image into a High Dynamic Range (HDR) output. |
| | |
| | 1. **Upload:** Use the 'Upload Bracketed LDR Images' box to upload **at least two** images of the same scene, taken at different exposures (bracketed). |
| | 2. **Run:** Click the **"Process Images"** button. |
| | 3. **Review:** |
| | * The model first runs an analysis to select the 'Best LDR'. |
| | * The selected LDR is then processed, and the 'Final HDR Image' will appear. |
| | """ |
| |
|
| | INPUT_EXPLANATION = """ |
| | ## 2. Input Requirements and Best Practices |
| | |
| | | Input Field | Purpose | Requirement | |
| | | :--- | :--- | :--- | |
| | | **LDR Images** | A set of images of the same scene captured with different exposure values (bracketing). | Must be 2 or more standard image files (JPG, PNG). | |
| | |
| | ### Best Practices for Input Images |
| | * **Bracketing is Key:** The quality of the final HDR output heavily depends on the diversity and quality of the input bracket set (underexposed, correctly exposed, and overexposed). |
| | * **Scene Consistency:** All uploaded images must be of the **exact same scene** and taken from the **exact same camera position** (tripod recommended). Motion between frames will lead to conversion artifacts. |
| | * **Resolution:** While the model processes images internally, uploading high-resolution sources ensures the final scaled 1024xN output maintains sharp detail. |
| | """ |
| |
|
| | TECHNICAL_GUIDANCE = """ |
| | ## 3. The Best LDR Selection Algorithm (Internal Logic) |
| | |
| | Unlike traditional HDR merging, this application first selects the single 'Best LDR' image from your uploads and then translates *that specific image* into HDR using a deep learning model. |
| | |
| | The selection process scores each image based on the following weighted metrics: |
| | |
| | | Metric | Weight | Description | |
| | | :--- | :--- | :--- | |
| | | **Clipped Pixels** | 35% | Penalizes images with over-saturated whites or completely black shadows. | |
| | | **Coverage** | 25% | Measures the range of usable tones across the image. | |
| | | **Exposure** | 15% | Measures closeness to ideal scene brightness. | |
| | | **Sharpness** | 15% | Measures overall clarity and focus of the image. | |
| | | **Noise** | 10% | Penalizes excessive grain or image noise. | |
| | |
| | The image with the highest composite score is chosen for the final AI conversion. |
| | """ |
| |
|
| | OUTPUT_EXPLANATION = """ |
| | ## 4. Expected Outputs and Interpretation |
| | |
| | | Output Field | Description | Guidance | |
| | | :--- | :--- | :--- | |
| | | **Uploaded Images** | A gallery showing all LDR images provided as input. | Confirms which files were successfully loaded and analyzed by the scoring algorithm. | |
| | | **Final HDR Image** | The resulting image generated by the **CycleGAN** translation model. | This image should exhibit enhanced detail in very bright and very dark areas, greater overall contrast, and richer color vibrancy compared to the original LDRs. | |
| | |
| | ### Note on Resolution |
| | The inference process scales the selected LDR image to **1024 pixels wide** internally, maintaining the original aspect ratio, before running the conversion model. The final output resolution will match this scaled size. |
| | """ |
| |
|
| | |
| |
|
| | |
| | if '--dataroot' not in sys.argv: |
| | sys.argv.extend(['--dataroot', './dummy_dataroot_for_init']) |
| |
|
| | |
| | opt = TestOptions().parse() |
| |
|
| | |
| | opt.name = 'ldr2hdr_cyclegan_728' |
| | opt.model = 'test' |
| | opt.netG = 'resnet_9blocks' |
| | opt.norm = 'instance' |
| | opt.no_dropout = True |
| | opt.checkpoints_dir = './checkpoints' |
| | opt.gpu_ids = [0] if torch.cuda.is_available() else [] |
| | opt.device = torch.device('cuda:{}'.format(opt.gpu_ids[0])) if opt.gpu_ids else torch.device('cpu') |
| |
|
| | |
| | model = create_model(opt) |
| | model.setup(opt) |
| | model.eval() |
| |
|
| | print("--- Model Loaded Successfully ---") |
| |
|
| |
|
| | |
| |
|
| | def process_images_to_hdr(list_of_temp_files): |
| | """ |
| | The main workflow: select best LDR, run inference, and return results for the UI. |
| | """ |
| | if not list_of_temp_files: |
| | raise gr.Error("Please upload your bracketed LDR images.") |
| | if len(list_of_temp_files) < 2: |
| | gr.Warning("For best results, upload at least 2 bracketed LDR images.") |
| |
|
| | uploaded_filepaths = [Path(f.name) for f in list_of_temp_files] |
| | |
| | try: |
| | |
| | print(f"Analyzing {len(uploaded_filepaths)} uploaded images...") |
| | weights = {"clipped": 0.35, "coverage": 0.25, "exposure": 0.15, "sharpness": 0.15, "noise": 0.10} |
| | records = compute_metrics_for_images(uploaded_filepaths, resize_max=1024) |
| | |
| | |
| | valid_records = [r for r in records if r is not None] |
| | if not valid_records: |
| | raise gr.Error("Could not process any uploaded images (ensure they are valid image files).") |
| |
|
| | scored_records = score_records(valid_records, weights) |
| | |
| | if not scored_records: |
| | |
| | raise gr.Error("Could not read or score any of the uploaded images.") |
| | |
| | best_ldr_record = scored_records[0] |
| | best_ldr_path = best_ldr_record['path'] |
| | print(f"Best LDR selected: {os.path.basename(best_ldr_path)} (Score: {best_ldr_record['score']:.4f})") |
| |
|
| | |
| | print("Running Full Image (High-Res Scaled) Inference...") |
| | |
| | |
| | inference_options = { |
| | 'preprocess': 'scale_width', |
| | 'load_size': 1024, |
| | 'crop_size': 728 |
| | } |
| | |
| | |
| | local_opt = copy.deepcopy(opt) |
| | local_opt.num_threads = 0 |
| | local_opt.batch_size = 1 |
| | local_opt.serial_batches = True |
| | for key, value in inference_options.items(): |
| | setattr(local_opt, key, value) |
| |
|
| | |
| | with tempfile.TemporaryDirectory() as temp_dir: |
| | shutil.copy(best_ldr_path, temp_dir) |
| | local_opt.dataroot = temp_dir |
| | local_opt.num_test = 1 |
| | dataset = create_dataset(local_opt) |
| |
|
| | for i, data in enumerate(dataset): |
| | model.set_input(data) |
| | model.test() |
| | visuals = model.get_current_visuals() |
| | |
| | for label, image_tensor in visuals.items(): |
| | if label == 'fake': |
| | image_numpy = (np.transpose(image_tensor.cpu().float().numpy()[0], (1, 2, 0)) + 1) / 2.0 * 255.0 |
| | final_hdr_image = Image.fromarray(image_numpy.astype(np.uint8)) |
| | print("Conversion to HDR successful.") |
| | |
| | return uploaded_filepaths, final_hdr_image |
| |
|
| | except Exception as e: |
| | print(f"An error occurred: {e}") |
| | raise gr.Error(f"An error occurred during processing: {e}") |
| |
|
| | |
| |
|
| | with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as demo: |
| | gr.Markdown( |
| | """ |
| | # LDR Bracketing to HDR Converter |
| | Upload a set of bracketed LDR images. The app will automatically select the best one and convert it to a vibrant, full-resolution HDR image. |
| | """ |
| | ) |
| | |
| | |
| | with gr.Accordion("Tips & User Guidelines", open=False): |
| | gr.Markdown(USAGE_GUIDELINES) |
| | gr.Markdown("---") |
| | gr.Markdown(INPUT_EXPLANATION) |
| | gr.Markdown("---") |
| | gr.Markdown(TECHNICAL_GUIDANCE) |
| | gr.Markdown("---") |
| | gr.Markdown(OUTPUT_EXPLANATION) |
| |
|
| | with gr.Row(): |
| | with gr.Column(scale=1): |
| | |
| | gr.Markdown("## Step 1: Upload LDR Images") |
| | input_files = gr.Files( |
| | label="Bracketed LDR Images", |
| | file_types=["image"] |
| | ) |
| | gr.Markdown("## Step 2: Click Process Images") |
| | process_button = gr.Button("Process Images", variant="primary") |
| |
|
| | |
| | |
| |
|
| | with gr.Column(scale=2): |
| | gr.Markdown("## Generated HDR Result") |
| | with gr.Accordion("See Your Uploaded Images", open=False): |
| | input_gallery = gr.Gallery(label="Uploaded Images", show_label=False, columns=[2, 3], height="auto") |
| | |
| | output_image = gr.Image(label="Final HDR Image", type="pil", interactive=False, show_download_button=True) |
| |
|
| | process_button.click( |
| | fn=process_images_to_hdr, |
| | inputs=input_files, |
| | outputs=[input_gallery, output_image] |
| | ) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) |
| | SAMPLE_DATA_DIR = os.path.join(BASE_DIR, "sample_data") |
| |
|
| | EXAMPLE_FILES = [ |
| | os.path.join(SAMPLE_DATA_DIR, "ldr5.jpg"), |
| | os.path.join(SAMPLE_DATA_DIR, "ldr2.jpeg"), |
| | os.path.join(SAMPLE_DATA_DIR, "ldr1.jpg"), |
| | os.path.join(SAMPLE_DATA_DIR, "ldr6.jpg"), |
| | ] |
| |
|
| | |
| |
|
| | gr.Markdown("### Examples") |
| | gr.Examples( |
| | |
| | |
| | |
| | examples=[ |
| | [EXAMPLE_FILES] |
| | ], |
| | inputs=[input_files], |
| | label="Click to load these LDR images" |
| | ) |
| |
|
| | print("--- Launching Gradio App ---") |
| | demo.launch( |
| | server_name="0.0.0.0", |
| | server_port=7860 |
| | ) |