From 1319696994134c13882b91f7510e89fdbd1219bd Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 15:14:54 +0200 Subject: [PATCH 01/11] add gitignore --- .gitignore | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b7fc50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,183 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + + +# custom files +Checkpoints/ +Images/ +__pycache__/ +dataset/ +libs/__pycache__/utils.cpython-310.pyc +libs/__pycache__/vgg16.cpython-310.pyc From 20c372bd9cfcb21f8308e8d2960becc7e7e4eaea Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 15:21:58 +0200 Subject: [PATCH 02/11] Stop tracking output.png --- .gitignore | 1 + static/output.png | Bin 67890 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 static/output.png diff --git a/.gitignore b/.gitignore index 0b7fc50..a0a15f4 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,4 @@ __pycache__/ dataset/ libs/__pycache__/utils.cpython-310.pyc libs/__pycache__/vgg16.cpython-310.pyc +static/output.png \ No newline at end of file diff --git a/static/output.png b/static/output.png deleted file mode 100644 index 107e8a5858fa38572c399e6b4b7dee7fd712075e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67890 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 i`=@h%x9uvg`2Pan*swFw1BuK40000 Date: Mon, 12 May 2025 15:24:15 +0200 Subject: [PATCH 03/11] upgrade tensorflow and scikit --- conv_helper.py | 63 +++++++++++++-------------- main.py | 9 ++-- model.py | 113 ++++++++++++++++++++++++++++++++++++++----------- test.py | 44 +++++++------------ train.py | 5 ++- utils.py | 17 ++++---- 6 files changed, 152 insertions(+), 99 deletions(-) diff --git a/conv_helper.py b/conv_helper.py index a5b1f13..d946e7d 100644 --- a/conv_helper.py +++ b/conv_helper.py @@ -1,39 +1,36 @@ import tensorflow as tf -import tensorflow.contrib.slim as slim - from utils import * -def conv_layer(input_image, ksize, in_channels, out_channels, stride, scope_name, activation_function=lrelu, reuse=False): - with tf.variable_scope(scope_name): - filter = tf.Variable(tf.random_normal([ksize, ksize, in_channels, out_channels], stddev=0.03)) - output = tf.nn.conv2d(input_image, filter, strides=[1, stride, stride, 1], padding='SAME') - output = slim.batch_norm(output) - if activation_function: - output = activation_function(output) - return output, filter - -def residual_layer(input_image, ksize, in_channels, out_channels, stride, scope_name): - with tf.variable_scope(scope_name): - output, filter = conv_layer(input_image, ksize, in_channels, out_channels, stride, scope_name+"_conv1") - output, filter = conv_layer(output, ksize, out_channels, out_channels, stride, scope_name+"_conv2") - output = tf.add(output, tf.identity(input_image)) - return output, filter - -def transpose_deconvolution_layer(input_tensor, used_weights, new_shape, stride, scope_name): - with tf.varaible_scope(scope_name): - output = tf.nn.conv2d_transpose(input_tensor, used_weights, output_shape=new_shape, strides=[1, stride, stride, 1], padding='SAME') - output = tf.nn.relu(output) - return output +class ResidualBlock(tf.keras.layers.Layer): + def __init__(self, out_channels, ksize, stride, name=None): + super(ResidualBlock, self).__init__(name=name) + self.conv1 = tf.keras.layers.Conv2D(out_channels, ksize, strides=stride, padding='same') + self.conv2 = tf.keras.layers.Conv2D(out_channels, ksize, strides=stride, padding='same') + self.bn1 = tf.keras.layers.BatchNormalization() + self.bn2 = tf.keras.layers.BatchNormalization() + self.activation = tf.keras.layers.LeakyReLU(0.2) + self.add = tf.keras.layers.Add() -def resize_deconvolution_layer(input_tensor, new_shape, scope_name): - with tf.variable_scope(scope_name): - output = tf.image.resize_images(input_tensor, (new_shape[1], new_shape[2]), method=1) - output, unused_weights = conv_layer(output, 3, new_shape[3]*2, new_shape[3], 1, scope_name+"_deconv") - return output + def call(self, inputs): + x = self.conv1(inputs) + x = self.bn1(x) + x = self.activation(x) + + x = self.conv2(x) + x = self.bn2(x) + x = self.activation(x) + + return self.add([x, inputs]) -def deconvolution_layer(input_tensor, new_shape, scope_name): - return resize_deconvolution_layer(input_tensor, new_shape, scope_name) +class DeconvolutionBlock(tf.keras.layers.Layer): + def __init__(self, out_channels, name=None): + super(DeconvolutionBlock, self).__init__(name=name) + self.conv = tf.keras.layers.Conv2D(out_channels, 3, padding='same') + self.bn = tf.keras.layers.BatchNormalization() + self.activation = tf.keras.layers.LeakyReLU(0.2) -def output_between_zero_and_one(output): - output +=1 - return output/2 + def call(self, inputs, target_height, target_width): + x = tf.image.resize(inputs, (target_height, target_width), method='bilinear') + x = self.conv(x) + x = self.bn(x) + return self.activation(x) diff --git a/main.py b/main.py index c488a8f..74bf2aa 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,10 @@ from flask import Flask, render_template, request, jsonify, send_file import numpy as np -import scipy.misc import base64 from io import BytesIO from test import * import time - +from PIL import Image app = Flask(__name__) @app.route('/') @@ -18,7 +17,11 @@ def denoisify(): if request.method == "POST": inputImg = request.files['file'] outputImg = denoise(inputImg) - scipy.misc.imsave('static/output.png', outputImg) + # scipy.misc.imsave('static/output.png', outputImg) + # If outputImg is a numpy array + output_image = (outputImg * 255).astype(np.uint8) # Denormalize if it's in [0, 1] range + Image.fromarray(output_image).save('static/output.png') + return jsonify(result="Success") diff --git a/model.py b/model.py index f075e44..83a8976 100644 --- a/model.py +++ b/model.py @@ -1,38 +1,103 @@ import numpy as np import tensorflow as tf -import tensorflow.contrib.slim as slim +from utils import * +from conv_helper import ResidualBlock, DeconvolutionBlock +class Generator(tf.keras.Model): + def __init__(self): + super(Generator, self).__init__() + self.conv1 = tf.keras.layers.Conv2D(32, 9, padding='same') + self.conv2 = tf.keras.layers.Conv2D(64, 3, padding='same') + self.conv3 = tf.keras.layers.Conv2D(128, 3, padding='same') + self.conv4 = tf.keras.layers.Conv2D(3, 9, padding='same', activation='tanh') + + self.bn1 = tf.keras.layers.BatchNormalization() + self.bn2 = tf.keras.layers.BatchNormalization() + self.bn3 = tf.keras.layers.BatchNormalization() + + self.activation = tf.keras.layers.LeakyReLU(0.2) + + # Create residual blocks + self.res_blocks = [ResidualBlock(128, 3, 1, name=f"g_res_{i}") for i in range(3)] + + # Create deconvolution blocks + self.deconv1 = DeconvolutionBlock(64, name='g_deconv1') + self.deconv2 = DeconvolutionBlock(32, name='g_deconv2') -from utils import * -from conv_helper import * + # Add layer for final normalization + self.add1 = tf.keras.layers.Add() + self.add2 = tf.keras.layers.Add() + + def normalize_output(self, x): + return (x + 1.0) / 2.0 + + def call(self, inputs): + x = self.conv1(inputs) + x = self.bn1(x) + x = self.activation(x) + conv1 = x + + x = self.conv2(x) + x = self.bn2(x) + x = self.activation(x) + + x = self.conv3(x) + x = self.bn3(x) + x = self.activation(x) + + # Residual blocks + for res_block in self.res_blocks: + x = res_block(x) + # Deconvolution layers + x = self.deconv1(x, target_height=128, target_width=128) + x = self.deconv2(x, target_height=256, target_width=256) + x = self.add1([x, conv1]) -def generator(input): - conv1, conv1_weights = conv_layer(input, 9, 3, 32, 1, "g_conv1") - conv2, conv2_weights = conv_layer(conv1, 3, 32, 64, 1, "g_conv2") - conv3, conv3_weights = conv_layer(conv2, 3, 64, 128, 1, "g_conv3") + x = self.conv4(x) + x = self.add2([x, inputs]) + return self.normalize_output(x) - res1, res1_weights = residual_layer(conv3, 3, 128, 128, 1, "g_res1") - res2, res2_weights = residual_layer(res1, 3, 128, 128, 1, "g_res2") - res3, res3_weights = residual_layer(res2, 3, 128, 128, 1, "g_res3") +class Discriminator(tf.keras.Model): + def __init__(self): + super(Discriminator, self).__init__() + self.conv1 = tf.keras.layers.Conv2D(48, 4, strides=2, padding='same') + self.conv2 = tf.keras.layers.Conv2D(96, 4, strides=2, padding='same') + self.conv3 = tf.keras.layers.Conv2D(192, 4, strides=2, padding='same') + self.conv4 = tf.keras.layers.Conv2D(384, 4, padding='same') + self.conv5 = tf.keras.layers.Conv2D(1, 4, padding='same', activation='sigmoid') + + self.bn1 = tf.keras.layers.BatchNormalization() + self.bn2 = tf.keras.layers.BatchNormalization() + self.bn3 = tf.keras.layers.BatchNormalization() + self.bn4 = tf.keras.layers.BatchNormalization() + + self.activation = tf.keras.layers.LeakyReLU(0.2) - deconv1 = deconvolution_layer(res3, [BATCH_SIZE, 128, 128, 64], 'g_deconv1') - deconv2 = deconvolution_layer(deconv1, [BATCH_SIZE, 256, 256, 32], "g_deconv2") + def call(self, inputs): + x = self.conv1(inputs) + x = self.bn1(x) + x = self.activation(x) - deconv2 = deconv2 + conv1 + x = self.conv2(x) + x = self.bn2(x) + x = self.activation(x) - conv4, conv4_weights = conv_layer(deconv2, 9, 32, 3, 1, "g_conv5", activation_function=tf.nn.tanh) + x = self.conv3(x) + x = self.bn3(x) + x = self.activation(x) - conv4 = conv4 + input - output = output_between_zero_and_one(conv4) + x = self.conv4(x) + x = self.bn4(x) + x = self.activation(x) - return output + return self.conv5(x) -def discriminator(input, reuse=False): - conv1, conv1_weights = conv_layer(input, 4, 3, 48, 2, "d_conv1", reuse=reuse) - conv2, conv2_weights = conv_layer(conv1, 4, 48, 96, 2, "d_conv2", reuse=reuse) - conv3, conv3_weights = conv_layer(conv2, 4, 96, 192, 2, "d_conv3", reuse=reuse) - conv4, conv4_weights = conv_layer(conv3, 4, 192, 384, 1, "d_conv4", reuse=reuse) - conv5, conv5_weights = conv_layer(conv4, 4, 384, 1, 1, "d_conv5", activation_function=tf.nn.sigmoid, reuse=reuse) +# Create model instances +generator = Generator() +discriminator = Discriminator() - return conv5 +# Build the models with a sample input to initialize weights +dummy_input = tf.zeros((1, 256, 256, 3)) +_ = generator(dummy_input) +_ = discriminator(dummy_input) diff --git a/test.py b/test.py index 37450a8..d9c4e96 100644 --- a/test.py +++ b/test.py @@ -4,43 +4,27 @@ import numpy as np from utils import * -from model import * +from model import generator, discriminator from skimage import measure +from PIL import Image def test(image): - tf.reset_default_graph() + tf.keras.backend.clear_session() - global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step') - - gen_in = tf.placeholder(shape=[None, BATCH_SHAPE[1], BATCH_SHAPE[2], BATCH_SHAPE[3]], dtype=tf.float32, name='generated_image') - real_in = tf.placeholder(shape=[None, BATCH_SHAPE[1], BATCH_SHAPE[2], BATCH_SHAPE[3]], dtype=tf.float32, name='groundtruth_image') - - Gz = generator(gen_in) - - - - init = tf.global_variables_initializer() - with tf.Session() as sess: - sess.run(init) - - saver = initialize(sess) - initial_step = global_step.eval() - - start_time = time.time() - n_batches = 200 - total_iteration = n_batches * N_EPOCHS - - image = sess.run(tf.map_fn(lambda img: tf.image.per_image_standardization(img), image)) - image = sess.run(Gz, feed_dict={gen_in: image}) - image = np.resize(image[0][56:, :, :], [144, 256, 3]) - imsave('output', image) - return image + # Process image + image = tf.image.per_image_standardization(image) + image = generator.predict(image) + image = np.resize(image[0][56:, :, :], [144, 256, 3]) + imsave('output', image) + return image def denoise(image): - image = scipy.misc.imread(image, mode='RGB').astype('float32') + # image = scipy.misc.imread(image, mode='RGB').astype('float32') + + image = np.array(Image.open(image).convert('RGB')).astype('float32') / 255.0 npad = ((56, 56), (0, 0), (0, 0)) image = np.pad(image, pad_width=npad, mode='constant', constant_values=0) image = np.expand_dims(image, axis=0) @@ -51,7 +35,9 @@ def denoise(image): if __name__=='__main__': - image = scipy.misc.imread(sys.argv[-1], mode='RGB').astype('float32') + # image = scipy.misc.imread(sys.argv[-1], mode='RGB').astype('float32') + image = np.array(Image.open(sys.argv[-1]).convert('RGB')).astype('float32') / 255.0 + npad = ((56, 56), (0, 0), (0, 0)) image = np.pad(image, pad_width=npad, mode='constant', constant_values=0) image = np.expand_dims(image, axis=0) diff --git a/train.py b/train.py index 5d8544b..f304007 100644 --- a/train.py +++ b/train.py @@ -7,6 +7,7 @@ from model import * from skimage import measure +from PIL import Image @@ -75,7 +76,9 @@ def train(): image = np.resize(image[7][56:, :, :], [144, 256, 3]) imsave('val_%d' % (index+1), image) - image = scipy.misc.imread(IMG_DIR+'val_%d.png' % (index+1), mode='RGB').astype('float32') + # image = scipy.misc.imread(IMG_DIR+'val_%d.png' % (index+1), mode='RGB').astype('float32') + image = np.array(Image.open(os.path.join(IMG_DIR, f'val_{index+1}.png')).convert('RGB')).astype('float32') / 255.0 + psnr = measure.compare_psnr(metrics_image, image, data_range=255) ssim = measure.compare_ssim(metrics_image, image, multichannel=True, data_range=255, win_size=11) diff --git a/utils.py b/utils.py index 6a105cd..779e96f 100644 --- a/utils.py +++ b/utils.py @@ -2,17 +2,13 @@ import re import sys import glob -import scipy.misc from itertools import cycle import numpy as np import tensorflow as tf - - -from libs import vgg16 - from PIL import Image +from libs import vgg16 LEARNING_RATE = 0.002 BATCH_SIZE = 5 @@ -31,7 +27,7 @@ PIXEL_LOSS_FACTOR = 1.0 STYLE_LOSS_FACTOR = 1.0 SMOOTH_LOSS_FACTOR = 1.0 -metrics_image = scipy.misc.imread(METRICS_SET_DIR+'gt.png', mode='RGB').astype('float32') +metrics_image = np.array(Image.open(METRICS_SET_DIR+'gt.png').convert('RGB')).astype('float32') def initialize(sess): @@ -72,14 +68,14 @@ def load_next_training_batch(): def load_validation(): filelist = sorted(glob.glob(VALIDATION_SET_DIR + '/*.png'), key=alphanum_key) - validation = np.array([np.array(scipy.misc.imread(fname, mode='RGB').astype('float32')) for fname in filelist]) + validation = np.array([np.array(Image.open(fname).convert('RGB')).astype('float32') for fname in filelist]) npad = ((0, 0), (56, 56), (0, 0), (0, 0)) validation = np.pad(validation, pad_width=npad, mode='constant', constant_values=0) return validation def training_dataset_init(): filelist = sorted(glob.glob(TRAINING_SET_DIR + '/*.png'), key=alphanum_key) - batch = np.array([np.array(scipy.misc.imread(fname, mode='RGB').astype('float32')) for fname in filelist]) + batch = np.array([np.array(Image.open(fname).convert('RGB')).astype('float32') for fname in filelist]) batch = split(batch, BATCH_SIZE) training_dir_list = get_training_dir_list() global pool @@ -88,7 +84,10 @@ def training_dataset_init(): def imsave(filename, image): - scipy.misc.imsave(IMG_DIR+filename+'.png', image) + # Create Images directory if it doesn't exist + if not os.path.exists(IMG_DIR): + os.makedirs(IMG_DIR) + Image.fromarray(np.uint8(image)).save(IMG_DIR+filename+'.png') def merge_images(file1, file2): """Merge two images into one, displayed side by side From bbc543428d53e60e7780b5e2908f511e3c8c23de Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 15:25:37 +0200 Subject: [PATCH 04/11] add requirements.txt --- requirements.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8184da0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +distlib==0.3.9 +filelock==3.18.0 +pbr==6.1.1 +platformdirs==4.3.8 +setuptools==80.3.1 +stevedore==5.4.1 +virtualenv==20.31.2 +virtualenv-clone==0.5.7 +virtualenvwrapper==6.1.1 From e19ed3c0cb30596445dd5c9293101bde1514f170 Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 16:16:03 +0200 Subject: [PATCH 05/11] update readme to reference requirements.txt --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0329155..ad614a0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Introduction -Animation movie companies like Pixar and Dreamworks render their 3d scenes using a technique called Pathtracing which enables them to create high quality photorealistic frames. Pathtracing involves shooting 1000’s of rays into a pixel randomly(Monte Carlo) which will then hit the objects in the scene and based on the reflective property of the object the rays reflect or refract or get absorbed. The colors returned by these rays are averaged to get the color of the pixel and this process is repeated for all the pixels. Due to the computational complexity it might take 8-16 hours to render a single frame. +Animation movie companies like Pixar and Dreamworks render their 3d scenes using a technique called Pathtracing which enables them to create high quality photorealistic frames. Pathtracing involves shooting 1000's of rays into a pixel randomly(Monte Carlo) which will then hit the objects in the scene and based on the reflective property of the object the rays reflect or refract or get absorbed. The colors returned by these rays are averaged to get the color of the pixel and this process is repeated for all the pixels. Due to the computational complexity it might take 8-16 hours to render a single frame. We are proposing a neural network based solution for reducing 8-16 hours to a couple of seconds using a Generative Adversarial Network. The main idea behind this proposed method is to render using small number of samples per pixel (let say 4 spp or 8 spp instead of 32K spp) and pass the noisy image to our network, which will generate a photorealistic image with high quality. @@ -11,8 +11,6 @@ We are proposing a neural network based solution for reducing 8-16 hours to a co [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/Yh_Bsoe-Qj4/0.jpg)](https://www.youtube.com/watch?v=Yh_Bsoe-Qj4) - - #### Table of Contents * [Installation](#installation) @@ -25,24 +23,65 @@ We are proposing a neural network based solution for reducing 8-16 hours to a co ## Installation -To run the project you will need: - * python 3.5 - * tensorflow (v1.1 or v1.0) - * PIL - * [CKPT FILE](https://uofi.box.com/shared/static/21a5jwdiqpnx24c50cyolwzwycnr3fwe.gz) - * [Dataset](https://uofi.box.com/shared/static/gy0t3vgwtlk1933xbtz1zvhlakkdac3n.zip) +### Prerequisites +* Python 3.5 or higher +* pip (Python package installer) +* virtualenv (recommended) + +### Setup Steps + +1. Clone the repository: +```bash +git clone https://github.com/yourusername/ImageDenoisingGAN.git +cd ImageDenoisingGAN +``` + +2. Create and activate a virtual environment: +```bash +# Create virtual environment +python -m venv venv + +# Activate virtual environment +# On Windows: +venv\Scripts\activate +# On macOS/Linux: +source venv/bin/activate +``` + +3. Install dependencies: +```bash +pip install -r requirements.txt +``` + +4. Download required files: +* [CKPT FILE](https://uofi.box.com/shared/static/21a5jwdiqpnx24c50cyolwzwycnr3fwe.gz) +* [Dataset](https://uofi.box.com/shared/static/gy0t3vgwtlk1933xbtz1zvhlakkdac3n.zip) (only if you want to train) + +### Required Files Structure +``` +ImageDenoisingGAN/ +├── venv/ # Virtual environment (created during setup) +├── Checkpoints/ # Extracted CKPT files go here +├── dataset/ # Dataset folder (if training) +├── static/ # Output images +└── requirements.txt # Project dependencies +``` ## Running -Once you have all the depenedencies ready, do the folowing: - -Download the dataset extract it to a folder named 'dataset' (ONLY if you want to train, not needed to run). +Once you have all the dependencies ready, do the following: -Extract the CKPT files to a folder named 'Checkpoints' +1. Extract the CKPT files to a folder named 'Checkpoints' -Run main.py -- python3 main.py +2. Run the application: +```bash +# Make sure your virtual environment is activated +python main.py +``` -Go to the browser, if you are running it on a server then [ip-address]:8888, if you are on your local machine then localhost:8888 +3. Access the application: +* If running locally: http://localhost:80 +* If running on a server: http://[server-ip]:80 ## Dataset We picked random 40 images from pixar movies, added gaussian noise of different standard deviation, 5 sets of 5 different standard deviation making a total of 1000 images for the training set. For validation we used 10 images completely different from the training set and added gaussian noise. For testing we had both added gaussian images and real noisy images. From ecfad525282f1316927332bc1c54fa7a55c824aa Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 16:16:53 +0200 Subject: [PATCH 06/11] update requirements.txt --- requirements.txt | 65 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8184da0..f015153 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,56 @@ -distlib==0.3.9 -filelock==3.18.0 -pbr==6.1.1 -platformdirs==4.3.8 -setuptools==80.3.1 -stevedore==5.4.1 -virtualenv==20.31.2 -virtualenv-clone==0.5.7 -virtualenvwrapper==6.1.1 +absl-py==2.2.2 +astunparse==1.6.3 +blinker==1.9.0 +certifi==2025.4.26 +charset-normalizer==3.4.2 +click==8.2.0 +contourpy==1.3.2 +cycler==0.12.1 +Flask==3.1.0 +flatbuffers==25.2.10 +fonttools==4.58.0 +gast==0.6.0 +google-pasta==0.2.0 +grpcio==1.71.0 +h5py==3.13.0 +idna==3.10 +imageio==2.37.0 +itsdangerous==2.2.0 +Jinja2==3.1.6 +keras==3.9.2 +kiwisolver==1.4.8 +lazy_loader==0.4 +libclang==18.1.1 +Markdown==3.8 +markdown-it-py==3.0.0 +MarkupSafe==3.0.2 +matplotlib==3.10.3 +mdurl==0.1.2 +ml_dtypes==0.5.1 +namex==0.0.9 +networkx==3.4.2 +numpy==2.1.3 +opencv-python==4.11.0.86 +opt_einsum==3.4.0 +optree==0.15.0 +packaging==25.0 +pillow==11.2.1 +protobuf==5.29.4 +Pygments==2.19.1 +pyparsing==3.2.3 +python-dateutil==2.9.0.post0 +requests==2.32.3 +rich==14.0.0 +scikit-image==0.25.2 +scipy==1.15.3 +six==1.17.0 +tensorboard==2.19.0 +tensorboard-data-server==0.7.2 +tensorflow==2.19.0 +tensorflow-io-gcs-filesystem==0.37.1 +termcolor==3.1.0 +tifffile==2025.5.10 +typing_extensions==4.13.2 +urllib3==2.4.0 +Werkzeug==3.1.3 +wrapt==1.17.2 From e7954923b35c537bdf4fc9ae4930d8a7df6ce095 Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 16:17:18 +0200 Subject: [PATCH 07/11] add new executable to use commandline for faster iteration --- commandline_main.py | 65 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 commandline_main.py diff --git a/commandline_main.py b/commandline_main.py new file mode 100644 index 0000000..2b66c99 --- /dev/null +++ b/commandline_main.py @@ -0,0 +1,65 @@ +import numpy as np +import cv2 +from test import * +import argparse + +def denoise_and_show(input_path): + """ + Denoises an image from the given file path and displays the input and output side by side. + + Args: + input_path (str): The path to the input noisy image. + """ + + try: + # 1. Load the image using OpenCV + input_img = cv2.imread(input_path) + input_img = cv2.cvtColor(input_img, cv2.COLOR_BGR2RGB) # Convert BGR to RGB + + # 2. Denoise the image + output_img = denoise(input_img) # Pass the numpy array directly + + # 3. Handle output image range (IMPORTANT) + if np.min(output_img) < 0 or np.max(output_img) > 1: + print("Warning: Output image values outside the expected range [0, 1]. Clipping.") + output_img = np.clip(output_img, 0, 1) + + output_img = (output_img * 255).astype(np.uint8) # Scale to 0-255 range + + # 4. Create side-by-side comparison + # Get dimensions + height1, width1 = input_img.shape[:2] + height2, width2 = output_img.shape[:2] + + # Create a new image with combined width + combined_width = width1 + width2 + combined_height = max(height1, height2) + combined_img = np.zeros((combined_height, combined_width, 3), dtype=np.uint8) + + # Copy images side by side + combined_img[:height1, :width1] = input_img + combined_img[:height2, width1:width1+width2] = output_img + + # Add labels + font = cv2.FONT_HERSHEY_SIMPLEX + cv2.putText(combined_img, "Original", (10, 30), font, 1, (255, 255, 255), 2) + cv2.putText(combined_img, "Denoised", (width1 + 10, 30), font, 1, (255, 255, 255), 2) + + # Save the comparison + cv2.imwrite('comparison.png', cv2.cvtColor(combined_img, cv2.COLOR_RGB2BGR)) + + # Show the combined image + cv2.imshow("Original vs Denoised", cv2.cvtColor(combined_img, cv2.COLOR_RGB2BGR)) + cv2.waitKey(0) + cv2.destroyAllWindows() + + except Exception as e: + print(f"Error processing image: {e}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Denoise an image from the command line.") + parser.add_argument("input_image", type=str, help="Path to the input noisy image.") + args = parser.parse_args() + + denoise_and_show(args.input_image) \ No newline at end of file From 96eb532946ea5d08a9d8cd3b66d8282dad7d0d94 Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 16:18:03 +0200 Subject: [PATCH 08/11] update gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a0a15f4..7acf623 100644 --- a/.gitignore +++ b/.gitignore @@ -181,4 +181,6 @@ __pycache__/ dataset/ libs/__pycache__/utils.cpython-310.pyc libs/__pycache__/vgg16.cpython-310.pyc -static/output.png \ No newline at end of file +static/output.png +.DS_Store +comparison.png From 287085635d9348f405675061b42e182966063eb7 Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 16:18:18 +0200 Subject: [PATCH 09/11] migrate from pil to opencv --- main.py | 22 ++++++++++++---------- test.py | 33 +++++++++++++++++++++------------ train.py | 22 +++++----------------- utils.py | 36 +++++++++++++++++------------------- 4 files changed, 55 insertions(+), 58 deletions(-) diff --git a/main.py b/main.py index 74bf2aa..553bc02 100644 --- a/main.py +++ b/main.py @@ -1,29 +1,31 @@ from flask import Flask, render_template, request, jsonify, send_file import numpy as np -import base64 from io import BytesIO from test import * -import time -from PIL import Image +import cv2 + app = Flask(__name__) @app.route('/') def index(): return render_template("index.html") - @app.route('/denoisify', methods=['GET', 'POST']) def denoisify(): if request.method == "POST": inputImg = request.files['file'] - outputImg = denoise(inputImg) - # scipy.misc.imsave('static/output.png', outputImg) - # If outputImg is a numpy array - output_image = (outputImg * 255).astype(np.uint8) # Denormalize if it's in [0, 1] range - Image.fromarray(output_image).save('static/output.png') + # Convert the file object to a numpy array using OpenCV + input_array = cv2.imdecode(np.frombuffer(inputImg.read(), np.uint8), cv2.IMREAD_COLOR) + input_array = cv2.cvtColor(input_array, cv2.COLOR_BGR2RGB) # Convert BGR to RGB + outputImg = denoise(input_array) + print(f"Out image: {outputImg}") + + # Save the output image using OpenCV + output_image = (outputImg * 255).astype(np.uint8) # Scale to 0-255 range + output_image = cv2.cvtColor(output_image, cv2.COLOR_RGB2BGR) # Convert RGB to BGR for saving + cv2.imwrite('static/output.png', output_image) return jsonify(result="Success") - if __name__=="__main__": app.run(host="0.0.0.0",port="80") diff --git a/test.py b/test.py index d9c4e96..748bc57 100644 --- a/test.py +++ b/test.py @@ -1,16 +1,11 @@ import time - import tensorflow as tf import numpy as np - +import cv2 from utils import * from model import generator, discriminator - from skimage import measure -from PIL import Image - - def test(image): tf.keras.backend.clear_session() @@ -22,9 +17,24 @@ def test(image): return image def denoise(image): - # image = scipy.misc.imread(image, mode='RGB').astype('float32') + # Handle both file paths and numpy arrays + if isinstance(image, str): + # If image is a file path + image = cv2.imread(image) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # Convert BGR to RGB + image = image.astype('float32') + elif isinstance(image, np.ndarray): + # If image is already a numpy array, ensure it's in the right format + if image.dtype != np.float32: + image = image.astype('float32') + if image.max() > 1.0: + image = image / 255.0 + # Ensure 3 channels (RGB) + if image.shape[-1] == 4: # If RGBA + image = image[..., :3] # Take only RGB channels + else: + raise ValueError("Input must be either a file path (str) or a numpy array") - image = np.array(Image.open(image).convert('RGB')).astype('float32') / 255.0 npad = ((56, 56), (0, 0), (0, 0)) image = np.pad(image, pad_width=npad, mode='constant', constant_values=0) image = np.expand_dims(image, axis=0) @@ -32,11 +42,10 @@ def denoise(image): output = test(image) return output - - if __name__=='__main__': - # image = scipy.misc.imread(sys.argv[-1], mode='RGB').astype('float32') - image = np.array(Image.open(sys.argv[-1]).convert('RGB')).astype('float32') / 255.0 + image = cv2.imread(sys.argv[-1]) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # Convert BGR to RGB + image = image.astype('float32') / 255.0 npad = ((56, 56), (0, 0), (0, 0)) image = np.pad(image, pad_width=npad, mode='constant', constant_values=0) diff --git a/train.py b/train.py index f304007..6223a25 100644 --- a/train.py +++ b/train.py @@ -1,15 +1,10 @@ import time - import tensorflow as tf import numpy as np - +import cv2 from utils import * from model import * - from skimage import measure -from PIL import Image - - def train(): tf.reset_default_graph() @@ -40,7 +35,6 @@ def train(): d_solver = tf.train.AdamOptimizer(LEARNING_RATE).minimize(d_loss, var_list=d_vars, global_step=global_step) g_solver = tf.train.AdamOptimizer(LEARNING_RATE).minimize(g_loss, var_list=g_vars) - init = tf.global_variables_initializer() with tf.Session() as sess: sess.run(init) @@ -54,7 +48,6 @@ def train(): validation_batch = sess.run(tf.map_fn(lambda img: tf.image.per_image_standardization(img), validation)) - for index in range(initial_step, total_iteration): input_batch = load_next_training_batch() training_batch, groundtruth_batch = np.split(input_batch, 2, axis=2) @@ -62,22 +55,19 @@ def train(): training_batch = sess.run(tf.map_fn(lambda img: tf.image.per_image_standardization(img), training_batch)) groundtruth_batch = sess.run(tf.map_fn(lambda img: tf.image.per_image_standardization(img), groundtruth_batch)) - _, d_loss_cur = sess.run([d_solver, d_loss], feed_dict={gen_in: training_batch, real_in: groundtruth_batch}) _, g_loss_cur = sess.run([g_solver, g_loss], feed_dict={gen_in: training_batch, real_in: groundtruth_batch}) - - - if(index + 1) % SKIP_STEP == 0: - saver.save(sess, CKPT_DIR, index) image = sess.run(Gz, feed_dict={gen_in: validation_batch}) image = np.resize(image[7][56:, :, :], [144, 256, 3]) imsave('val_%d' % (index+1), image) - # image = scipy.misc.imread(IMG_DIR+'val_%d.png' % (index+1), mode='RGB').astype('float32') - image = np.array(Image.open(os.path.join(IMG_DIR, f'val_{index+1}.png')).convert('RGB')).astype('float32') / 255.0 + + # Load the saved image using OpenCV + image = cv2.imread(os.path.join(IMG_DIR, f'val_{index+1}.png')) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype('float32') / 255.0 psnr = measure.compare_psnr(metrics_image, image, data_range=255) ssim = measure.compare_ssim(metrics_image, image, multichannel=True, data_range=255, win_size=11) @@ -86,8 +76,6 @@ def train(): "Step {}/{} Gen Loss: ".format(index + 1, total_iteration) + str(g_loss_cur) + " Disc Loss: " + str( d_loss_cur)+ " PSNR: "+str(psnr)+" SSIM: "+str(ssim)) - - if __name__=='__main__': training_dir_list = training_dataset_init() validation = load_validation() diff --git a/utils.py b/utils.py index 779e96f..4cc72bf 100644 --- a/utils.py +++ b/utils.py @@ -6,7 +6,7 @@ import numpy as np import tensorflow as tf -from PIL import Image +import cv2 from libs import vgg16 @@ -27,7 +27,8 @@ PIXEL_LOSS_FACTOR = 1.0 STYLE_LOSS_FACTOR = 1.0 SMOOTH_LOSS_FACTOR = 1.0 -metrics_image = np.array(Image.open(METRICS_SET_DIR+'gt.png').convert('RGB')).astype('float32') +metrics_image = cv2.imread(METRICS_SET_DIR+'gt.png') +metrics_image = cv2.cvtColor(metrics_image, cv2.COLOR_BGR2RGB).astype('float32') def initialize(sess): @@ -68,14 +69,14 @@ def load_next_training_batch(): def load_validation(): filelist = sorted(glob.glob(VALIDATION_SET_DIR + '/*.png'), key=alphanum_key) - validation = np.array([np.array(Image.open(fname).convert('RGB')).astype('float32') for fname in filelist]) + validation = np.array([cv2.cvtColor(cv2.imread(fname), cv2.COLOR_BGR2RGB).astype('float32') for fname in filelist]) npad = ((0, 0), (56, 56), (0, 0), (0, 0)) validation = np.pad(validation, pad_width=npad, mode='constant', constant_values=0) return validation def training_dataset_init(): filelist = sorted(glob.glob(TRAINING_SET_DIR + '/*.png'), key=alphanum_key) - batch = np.array([np.array(Image.open(fname).convert('RGB')).astype('float32') for fname in filelist]) + batch = np.array([cv2.cvtColor(cv2.imread(fname), cv2.COLOR_BGR2RGB).astype('float32') for fname in filelist]) batch = split(batch, BATCH_SIZE) training_dir_list = get_training_dir_list() global pool @@ -87,26 +88,25 @@ def imsave(filename, image): # Create Images directory if it doesn't exist if not os.path.exists(IMG_DIR): os.makedirs(IMG_DIR) - Image.fromarray(np.uint8(image)).save(IMG_DIR+filename+'.png') + # Convert RGB to BGR for OpenCV + image_bgr = cv2.cvtColor(np.uint8(image), cv2.COLOR_RGB2BGR) + cv2.imwrite(IMG_DIR+filename+'.png', image_bgr) def merge_images(file1, file2): """Merge two images into one, displayed side by side - :param file1: path to first image file - :param file2: path to second image file - :return: the merged Image object + :param file1: first image array + :param file2: second image array + :return: the merged image array """ - image1 = Image.fromarray(np.uint8(file1)) - image2 = Image.fromarray(np.uint8(file2)) - - (width1, height1) = image1.size - (width2, height2) = image2.size + height1, width1 = file1.shape[:2] + height2, width2 = file2.shape[:2] result_width = width1 + width2 result_height = max(height1, height2) - result = Image.new('RGB', (result_width, result_height)) - result.paste(im=image1, box=(0, 0)) - result.paste(im=image2, box=(width1, 0)) + result = np.zeros((result_height, result_width, 3), dtype=np.uint8) + result[:height1, :width1] = file1 + result[:height2, width1:width1+width2] = file2 return result @@ -140,9 +140,7 @@ def lrelu(x, leak=0.2, name='lrelu'): return f1 * x + f2 * abs(x) def RGB_TO_BGR(img): - img_channel_swap = img[..., ::-1] - # img_channel_swap_1 = tf.reverse(img, axis=[-1]) - return img_channel_swap + return cv2.cvtColor(img, cv2.COLOR_RGB2BGR) def get_pixel_loss(target,prediction): From 9b33f555041d526981d4aa58f33e45d410119fae Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 16:54:57 +0200 Subject: [PATCH 10/11] add new denoise script --- denoise_script.py | 210 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 denoise_script.py diff --git a/denoise_script.py b/denoise_script.py new file mode 100644 index 0000000..6036e9d --- /dev/null +++ b/denoise_script.py @@ -0,0 +1,210 @@ +# denoise_inference.py + +import time +import tensorflow as tf +import numpy as np +import cv2 +import sys # For command line arguments and exit + +# --- User-Provided Modules --- +try: + from utils import imsave + # 'generator' should be the instantiated Generator class from model.py + # model.py should handle any weight loading if necessary when 'generator' instance is created/imported + from model import generator +except ImportError as e: + print(f"Error importing 'utils' or 'model': {e}") + print("Please ensure 'utils.py' and 'model.py' (which defines and instantiates 'generator') are accessible.") + sys.exit(1) + +# --- Configuration --- +TARGET_OUTPUT_HEIGHT = 144 +TARGET_OUTPUT_WIDTH = 256 +PADDING_VAL = 56 + +def preprocess_padded_input_for_generator(padded_image_np_rgb_float32): + """ + Applies training-consistent standardization to a padded image and adds batch dimension. + """ + print(f"DEBUG: Padded input to standardize - Min: {padded_image_np_rgb_float32.min():.4f}, Max: {padded_image_np_rgb_float32.max():.4f}, Shape: {padded_image_np_rgb_float32.shape}, Dtype: {padded_image_np_rgb_float32.dtype}") + standardized_image_tensor = tf.image.per_image_standardization(padded_image_np_rgb_float32) + try: + standardized_image_np = standardized_image_tensor.numpy() + except AttributeError: + print("WARNING: .numpy() failed. Attempting to run in a new TF1 session.") + with tf.compat.v1.Session() as sess: + standardized_image_np = sess.run(standardized_image_tensor) + print(f"DEBUG: After standardization - Min: {standardized_image_np.min():.4f}, Max: {standardized_image_np.max():.4f}, Shape: {standardized_image_np.shape}") + batched_image = np.expand_dims(standardized_image_np, axis=0) + print(f"DEBUG: After adding batch dim - Shape: {batched_image.shape}, Dtype: {batched_image.dtype}") + return batched_image + +def postprocess_generator_output(output_tensor_batch): + """Converts generator's output batch to a displayable image (H, W, C) in [0,1] range.""" + if output_tensor_batch is None or output_tensor_batch.size == 0: + print("ERROR: Generator output is empty or None.") + return np.zeros((TARGET_OUTPUT_HEIGHT, TARGET_OUTPUT_WIDTH, 3), dtype=np.float32) + + img_from_generator = output_tensor_batch[0] # Remove batch dim + # **IMPORTANT**: Your generator model ALREADY scales its output to [0,1] range + # using its internal normalize_output((tanh_out + skip_input_standardized + 1.0)/2.0) + # So, img_from_generator should already be [0,1]. + print(f"DEBUG: Raw generator output (SHOULD BE [0,1] from model) - Min: {img_from_generator.min():.4f}, Max: {img_from_generator.max():.4f}, Shape: {img_from_generator.shape}, Dtype: {img_from_generator.dtype}") + + if img_from_generator.shape[0] <= PADDING_VAL: + print(f"ERROR: Image height ({img_from_generator.shape[0]}) is <= PADDING_VAL ({PADDING_VAL}). Cannot crop.") + img_cropped = img_from_generator + else: + img_cropped = img_from_generator[PADDING_VAL:, :, :] + print(f"DEBUG: After crop [{PADDING_VAL}:,:,:] - Min: {img_cropped.min():.4f}, Max: {img_cropped.max():.4f}, Shape: {img_cropped.shape}") + + # The [0,1] scaling is ALREADY DONE by the generator. We just need to ensure it's clipped. + img_processed = np.clip(img_cropped, 0.0, 1.0) + print(f"DEBUG: After clipping (generator output already [0,1]) - Min: {img_processed.min():.4f}, Max: {img_processed.max():.4f}") + + if img_processed.shape[0] != TARGET_OUTPUT_HEIGHT or img_processed.shape[1] != TARGET_OUTPUT_WIDTH: + img_resized = cv2.resize(img_processed, (TARGET_OUTPUT_WIDTH, TARGET_OUTPUT_HEIGHT), interpolation=cv2.INTER_AREA) + else: + img_resized = img_processed + print(f"DEBUG: After cv2.resize to target - Min: {img_resized.min():.4f}, Max: {img_resized.max():.4f}, Shape: {img_resized.shape}, Dtype: {img_resized.dtype}") + + return img_resized + +def denoise_image_inference(image_path_or_np_array): + if isinstance(image_path_or_np_array, str): + image_np = cv2.imread(image_path_or_np_array) + if image_np is None: + raise FileNotFoundError(f"ERROR: Image not found at '{image_path_or_np_array}'") + image_np_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB) + elif isinstance(image_path_or_np_array, np.ndarray): + image_np_rgb = image_path_or_np_array + if image_np_rgb.ndim == 2: + image_np_rgb = cv2.cvtColor(image_np_rgb, cv2.COLOR_GRAY2RGB) + if image_np_rgb.shape[-1] == 4: + image_np_rgb = image_np_rgb[..., :3] + else: + raise ValueError("ERROR: Input must be either a file path (str) or a numpy array") + + image_np_rgb_float32 = image_np_rgb.astype('float32') if image_np_rgb.dtype != np.float32 else image_np_rgb + print(f"DEBUG: Input image loaded - Shape: {image_np_rgb_float32.shape}, Dtype: {image_np_rgb_float32.dtype}, Min: {image_np_rgb_float32.min():.0f}, Max: {image_np_rgb_float32.max():.0f}") + + npad = ((PADDING_VAL, PADDING_VAL), (0, 0), (0, 0)) + padded_image = np.pad(image_np_rgb_float32, pad_width=npad, mode='constant', constant_values=0) + + generator_input_batch = preprocess_padded_input_for_generator(padded_image) + + print(f"INFO: Feeding to generator.predict() - Input Shape: {generator_input_batch.shape}, Input Dtype: {generator_input_batch.dtype}") + start_time = time.time() + try: + # Ensure 'generator' is the instantiated and (if necessary) weight-loaded model + generated_output_batch = generator.predict(generator_input_batch) + except Exception as e: + print(f"ERROR: generator.predict() failed: {e}") + traceback.print_exc() + return None + print(f"INFO: Denoising (generator.predict) took {time.time() - start_time:.4f} seconds.") + + final_image_0_1_range = postprocess_generator_output(generated_output_batch) + + output_filename_utils = 'output_denoised_utils.png' + try: + imsave(output_filename_utils, final_image_0_1_range) + print(f"INFO: Denoised image saved as '{output_filename_utils}' using utils.imsave.") + except Exception as e: + print(f"ERROR: utils.imsave failed: {e}. Attempting PIL save.") + output_filename_pil = 'output_denoised_pil_fallback.png' + try: + from PIL import Image as PILImage + img_to_save_uint8 = (final_image_0_1_range * 255.0).astype(np.uint8) + pil_img = PILImage.fromarray(img_to_save_uint8) + pil_img.save(output_filename_pil) + print(f"INFO: Denoised image saved as '{output_filename_pil}' using PIL.") + except Exception as pil_e: + print(f"ERROR: Saving with PIL also failed: {pil_e}") + + return final_image_0_1_range + +if __name__=='__main__': + if len(sys.argv) < 2: + print("Usage: python denoise_inference.py ") + sys.exit(1) + + input_image_path = sys.argv[1] + + print("INFO: TensorFlow Version:", tf.__version__) + # print("INFO: Eager execution enabled:", tf.executing_eagerly()) # .numpy() needs this or session + print(f"INFO: Attempting to denoise: {input_image_path}") + + # --- CRITICAL: Model Loading Assurance --- + # Your model.py does: + # generator = Generator() + # _ = generator(dummy_input) # This builds the model + # This means 'from model import generator' should give you an initialized model. + # If you save/load weights, that needs to happen in model.py or here. + # For now, assuming 'generator' from model.py is ready with trained weights. + if 'generator' not in globals() or generator is None: + print("CRITICAL ERROR: The 'generator' model instance from 'model.py' is not available or None.") + print(" Please ensure model.py defines and instantiates 'generator' correctly,") + print(" and that it has its trained weights loaded if you are not training now.") + sys.exit(1) + if not generator.built: + print("WARNING: Generator model does not seem to be built. Did you load weights or call it on dummy input?") + # Attempt to build if not built, using a shape that matches padded input later + # This is a guess for BATCH_SHAPE[1], BATCH_SHAPE[2] + # Padded height will be original_H + 2*PADDING_VAL, padded_W will be original_W + # This dummy build might not be perfect if original image sizes vary a lot. + # The dummy input in your model.py (256,256,3) is better. + # Let's assume the padding logic creates something compatible with that (256, W, 3) + dummy_padded_height = 100 + 2 * PADDING_VAL # e.g. 100 is an arbitrary original height + dummy_padded_width = 100 # e.g. 100 is an arbitrary original width + try: + print(f"Attempting to build generator with dummy input of shape (1, {dummy_padded_height}, {dummy_padded_width}, 3)") + # The model.py uses (1, 256, 256, 3) for its dummy build. This is good. + # The preprocess_padded_input_for_generator will create the actual input shape. + # We just need to ensure the model instance is "built" so predict can be called. + # The dummy call in model.py should have handled this. + pass # Assuming model.py's dummy call built it. + except Exception as build_e: + print(f"ERROR trying to build generator: {build_e}") + + + # --- End Model Loading Assurance --- + + import traceback # For more detailed error messages + try: + denoised_output_0_1_range = denoise_image_inference(input_image_path) + + if denoised_output_0_1_range is not None: + print("INFO: Denoising process completed.") + try: + original_display = cv2.imread(input_image_path) + if original_display is None: + print(f"WARNING: Could not read original for display: {input_image_path}") + else: + denoised_display_uint8_rgb = (denoised_output_0_1_range * 255.0).astype(np.uint8) + denoised_display_bgr = cv2.cvtColor(denoised_display_uint8_rgb, cv2.COLOR_RGB2BGR) + h_denoised, w_denoised = denoised_display_bgr.shape[:2] + h_orig, w_orig = original_display.shape[:2] + if h_orig != h_denoised: + scale_factor = h_denoised / h_orig + new_w_orig = int(w_orig * scale_factor) + original_display_resized = cv2.resize(original_display, (new_w_orig, h_denoised)) + else: + original_display_resized = original_display + comparison_img = np.concatenate((original_display_resized, denoised_display_bgr), axis=1) + cv2.imshow("Original (Resized) vs. Denoised", comparison_img) + print("INFO: Displaying comparison. Press any key to close.") + cv2.waitKey(0) + cv2.destroyAllWindows() + except Exception as display_e: + print(f"WARNING: Could not display images: {display_e}") + else: + print("ERROR: Denoising returned None.") + + except FileNotFoundError as e: + print(e) + except ValueError as e: + print(e) + except Exception as e: + print(f"CRITICAL ERROR in __main__: {e}") + traceback.print_exc() \ No newline at end of file From 4efcc1f535b527d6484255fb5861b5e04752fb55 Mon Sep 17 00:00:00 2001 From: Julian Eiler Date: Mon, 12 May 2025 17:17:41 +0200 Subject: [PATCH 11/11] remove useless stuff --- commandline_main.py | 65 -------------- denoise_script.py | 210 -------------------------------------------- 2 files changed, 275 deletions(-) delete mode 100644 commandline_main.py delete mode 100644 denoise_script.py diff --git a/commandline_main.py b/commandline_main.py deleted file mode 100644 index 2b66c99..0000000 --- a/commandline_main.py +++ /dev/null @@ -1,65 +0,0 @@ -import numpy as np -import cv2 -from test import * -import argparse - -def denoise_and_show(input_path): - """ - Denoises an image from the given file path and displays the input and output side by side. - - Args: - input_path (str): The path to the input noisy image. - """ - - try: - # 1. Load the image using OpenCV - input_img = cv2.imread(input_path) - input_img = cv2.cvtColor(input_img, cv2.COLOR_BGR2RGB) # Convert BGR to RGB - - # 2. Denoise the image - output_img = denoise(input_img) # Pass the numpy array directly - - # 3. Handle output image range (IMPORTANT) - if np.min(output_img) < 0 or np.max(output_img) > 1: - print("Warning: Output image values outside the expected range [0, 1]. Clipping.") - output_img = np.clip(output_img, 0, 1) - - output_img = (output_img * 255).astype(np.uint8) # Scale to 0-255 range - - # 4. Create side-by-side comparison - # Get dimensions - height1, width1 = input_img.shape[:2] - height2, width2 = output_img.shape[:2] - - # Create a new image with combined width - combined_width = width1 + width2 - combined_height = max(height1, height2) - combined_img = np.zeros((combined_height, combined_width, 3), dtype=np.uint8) - - # Copy images side by side - combined_img[:height1, :width1] = input_img - combined_img[:height2, width1:width1+width2] = output_img - - # Add labels - font = cv2.FONT_HERSHEY_SIMPLEX - cv2.putText(combined_img, "Original", (10, 30), font, 1, (255, 255, 255), 2) - cv2.putText(combined_img, "Denoised", (width1 + 10, 30), font, 1, (255, 255, 255), 2) - - # Save the comparison - cv2.imwrite('comparison.png', cv2.cvtColor(combined_img, cv2.COLOR_RGB2BGR)) - - # Show the combined image - cv2.imshow("Original vs Denoised", cv2.cvtColor(combined_img, cv2.COLOR_RGB2BGR)) - cv2.waitKey(0) - cv2.destroyAllWindows() - - except Exception as e: - print(f"Error processing image: {e}") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Denoise an image from the command line.") - parser.add_argument("input_image", type=str, help="Path to the input noisy image.") - args = parser.parse_args() - - denoise_and_show(args.input_image) \ No newline at end of file diff --git a/denoise_script.py b/denoise_script.py deleted file mode 100644 index 6036e9d..0000000 --- a/denoise_script.py +++ /dev/null @@ -1,210 +0,0 @@ -# denoise_inference.py - -import time -import tensorflow as tf -import numpy as np -import cv2 -import sys # For command line arguments and exit - -# --- User-Provided Modules --- -try: - from utils import imsave - # 'generator' should be the instantiated Generator class from model.py - # model.py should handle any weight loading if necessary when 'generator' instance is created/imported - from model import generator -except ImportError as e: - print(f"Error importing 'utils' or 'model': {e}") - print("Please ensure 'utils.py' and 'model.py' (which defines and instantiates 'generator') are accessible.") - sys.exit(1) - -# --- Configuration --- -TARGET_OUTPUT_HEIGHT = 144 -TARGET_OUTPUT_WIDTH = 256 -PADDING_VAL = 56 - -def preprocess_padded_input_for_generator(padded_image_np_rgb_float32): - """ - Applies training-consistent standardization to a padded image and adds batch dimension. - """ - print(f"DEBUG: Padded input to standardize - Min: {padded_image_np_rgb_float32.min():.4f}, Max: {padded_image_np_rgb_float32.max():.4f}, Shape: {padded_image_np_rgb_float32.shape}, Dtype: {padded_image_np_rgb_float32.dtype}") - standardized_image_tensor = tf.image.per_image_standardization(padded_image_np_rgb_float32) - try: - standardized_image_np = standardized_image_tensor.numpy() - except AttributeError: - print("WARNING: .numpy() failed. Attempting to run in a new TF1 session.") - with tf.compat.v1.Session() as sess: - standardized_image_np = sess.run(standardized_image_tensor) - print(f"DEBUG: After standardization - Min: {standardized_image_np.min():.4f}, Max: {standardized_image_np.max():.4f}, Shape: {standardized_image_np.shape}") - batched_image = np.expand_dims(standardized_image_np, axis=0) - print(f"DEBUG: After adding batch dim - Shape: {batched_image.shape}, Dtype: {batched_image.dtype}") - return batched_image - -def postprocess_generator_output(output_tensor_batch): - """Converts generator's output batch to a displayable image (H, W, C) in [0,1] range.""" - if output_tensor_batch is None or output_tensor_batch.size == 0: - print("ERROR: Generator output is empty or None.") - return np.zeros((TARGET_OUTPUT_HEIGHT, TARGET_OUTPUT_WIDTH, 3), dtype=np.float32) - - img_from_generator = output_tensor_batch[0] # Remove batch dim - # **IMPORTANT**: Your generator model ALREADY scales its output to [0,1] range - # using its internal normalize_output((tanh_out + skip_input_standardized + 1.0)/2.0) - # So, img_from_generator should already be [0,1]. - print(f"DEBUG: Raw generator output (SHOULD BE [0,1] from model) - Min: {img_from_generator.min():.4f}, Max: {img_from_generator.max():.4f}, Shape: {img_from_generator.shape}, Dtype: {img_from_generator.dtype}") - - if img_from_generator.shape[0] <= PADDING_VAL: - print(f"ERROR: Image height ({img_from_generator.shape[0]}) is <= PADDING_VAL ({PADDING_VAL}). Cannot crop.") - img_cropped = img_from_generator - else: - img_cropped = img_from_generator[PADDING_VAL:, :, :] - print(f"DEBUG: After crop [{PADDING_VAL}:,:,:] - Min: {img_cropped.min():.4f}, Max: {img_cropped.max():.4f}, Shape: {img_cropped.shape}") - - # The [0,1] scaling is ALREADY DONE by the generator. We just need to ensure it's clipped. - img_processed = np.clip(img_cropped, 0.0, 1.0) - print(f"DEBUG: After clipping (generator output already [0,1]) - Min: {img_processed.min():.4f}, Max: {img_processed.max():.4f}") - - if img_processed.shape[0] != TARGET_OUTPUT_HEIGHT or img_processed.shape[1] != TARGET_OUTPUT_WIDTH: - img_resized = cv2.resize(img_processed, (TARGET_OUTPUT_WIDTH, TARGET_OUTPUT_HEIGHT), interpolation=cv2.INTER_AREA) - else: - img_resized = img_processed - print(f"DEBUG: After cv2.resize to target - Min: {img_resized.min():.4f}, Max: {img_resized.max():.4f}, Shape: {img_resized.shape}, Dtype: {img_resized.dtype}") - - return img_resized - -def denoise_image_inference(image_path_or_np_array): - if isinstance(image_path_or_np_array, str): - image_np = cv2.imread(image_path_or_np_array) - if image_np is None: - raise FileNotFoundError(f"ERROR: Image not found at '{image_path_or_np_array}'") - image_np_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB) - elif isinstance(image_path_or_np_array, np.ndarray): - image_np_rgb = image_path_or_np_array - if image_np_rgb.ndim == 2: - image_np_rgb = cv2.cvtColor(image_np_rgb, cv2.COLOR_GRAY2RGB) - if image_np_rgb.shape[-1] == 4: - image_np_rgb = image_np_rgb[..., :3] - else: - raise ValueError("ERROR: Input must be either a file path (str) or a numpy array") - - image_np_rgb_float32 = image_np_rgb.astype('float32') if image_np_rgb.dtype != np.float32 else image_np_rgb - print(f"DEBUG: Input image loaded - Shape: {image_np_rgb_float32.shape}, Dtype: {image_np_rgb_float32.dtype}, Min: {image_np_rgb_float32.min():.0f}, Max: {image_np_rgb_float32.max():.0f}") - - npad = ((PADDING_VAL, PADDING_VAL), (0, 0), (0, 0)) - padded_image = np.pad(image_np_rgb_float32, pad_width=npad, mode='constant', constant_values=0) - - generator_input_batch = preprocess_padded_input_for_generator(padded_image) - - print(f"INFO: Feeding to generator.predict() - Input Shape: {generator_input_batch.shape}, Input Dtype: {generator_input_batch.dtype}") - start_time = time.time() - try: - # Ensure 'generator' is the instantiated and (if necessary) weight-loaded model - generated_output_batch = generator.predict(generator_input_batch) - except Exception as e: - print(f"ERROR: generator.predict() failed: {e}") - traceback.print_exc() - return None - print(f"INFO: Denoising (generator.predict) took {time.time() - start_time:.4f} seconds.") - - final_image_0_1_range = postprocess_generator_output(generated_output_batch) - - output_filename_utils = 'output_denoised_utils.png' - try: - imsave(output_filename_utils, final_image_0_1_range) - print(f"INFO: Denoised image saved as '{output_filename_utils}' using utils.imsave.") - except Exception as e: - print(f"ERROR: utils.imsave failed: {e}. Attempting PIL save.") - output_filename_pil = 'output_denoised_pil_fallback.png' - try: - from PIL import Image as PILImage - img_to_save_uint8 = (final_image_0_1_range * 255.0).astype(np.uint8) - pil_img = PILImage.fromarray(img_to_save_uint8) - pil_img.save(output_filename_pil) - print(f"INFO: Denoised image saved as '{output_filename_pil}' using PIL.") - except Exception as pil_e: - print(f"ERROR: Saving with PIL also failed: {pil_e}") - - return final_image_0_1_range - -if __name__=='__main__': - if len(sys.argv) < 2: - print("Usage: python denoise_inference.py ") - sys.exit(1) - - input_image_path = sys.argv[1] - - print("INFO: TensorFlow Version:", tf.__version__) - # print("INFO: Eager execution enabled:", tf.executing_eagerly()) # .numpy() needs this or session - print(f"INFO: Attempting to denoise: {input_image_path}") - - # --- CRITICAL: Model Loading Assurance --- - # Your model.py does: - # generator = Generator() - # _ = generator(dummy_input) # This builds the model - # This means 'from model import generator' should give you an initialized model. - # If you save/load weights, that needs to happen in model.py or here. - # For now, assuming 'generator' from model.py is ready with trained weights. - if 'generator' not in globals() or generator is None: - print("CRITICAL ERROR: The 'generator' model instance from 'model.py' is not available or None.") - print(" Please ensure model.py defines and instantiates 'generator' correctly,") - print(" and that it has its trained weights loaded if you are not training now.") - sys.exit(1) - if not generator.built: - print("WARNING: Generator model does not seem to be built. Did you load weights or call it on dummy input?") - # Attempt to build if not built, using a shape that matches padded input later - # This is a guess for BATCH_SHAPE[1], BATCH_SHAPE[2] - # Padded height will be original_H + 2*PADDING_VAL, padded_W will be original_W - # This dummy build might not be perfect if original image sizes vary a lot. - # The dummy input in your model.py (256,256,3) is better. - # Let's assume the padding logic creates something compatible with that (256, W, 3) - dummy_padded_height = 100 + 2 * PADDING_VAL # e.g. 100 is an arbitrary original height - dummy_padded_width = 100 # e.g. 100 is an arbitrary original width - try: - print(f"Attempting to build generator with dummy input of shape (1, {dummy_padded_height}, {dummy_padded_width}, 3)") - # The model.py uses (1, 256, 256, 3) for its dummy build. This is good. - # The preprocess_padded_input_for_generator will create the actual input shape. - # We just need to ensure the model instance is "built" so predict can be called. - # The dummy call in model.py should have handled this. - pass # Assuming model.py's dummy call built it. - except Exception as build_e: - print(f"ERROR trying to build generator: {build_e}") - - - # --- End Model Loading Assurance --- - - import traceback # For more detailed error messages - try: - denoised_output_0_1_range = denoise_image_inference(input_image_path) - - if denoised_output_0_1_range is not None: - print("INFO: Denoising process completed.") - try: - original_display = cv2.imread(input_image_path) - if original_display is None: - print(f"WARNING: Could not read original for display: {input_image_path}") - else: - denoised_display_uint8_rgb = (denoised_output_0_1_range * 255.0).astype(np.uint8) - denoised_display_bgr = cv2.cvtColor(denoised_display_uint8_rgb, cv2.COLOR_RGB2BGR) - h_denoised, w_denoised = denoised_display_bgr.shape[:2] - h_orig, w_orig = original_display.shape[:2] - if h_orig != h_denoised: - scale_factor = h_denoised / h_orig - new_w_orig = int(w_orig * scale_factor) - original_display_resized = cv2.resize(original_display, (new_w_orig, h_denoised)) - else: - original_display_resized = original_display - comparison_img = np.concatenate((original_display_resized, denoised_display_bgr), axis=1) - cv2.imshow("Original (Resized) vs. Denoised", comparison_img) - print("INFO: Displaying comparison. Press any key to close.") - cv2.waitKey(0) - cv2.destroyAllWindows() - except Exception as display_e: - print(f"WARNING: Could not display images: {display_e}") - else: - print("ERROR: Denoising returned None.") - - except FileNotFoundError as e: - print(e) - except ValueError as e: - print(e) - except Exception as e: - print(f"CRITICAL ERROR in __main__: {e}") - traceback.print_exc() \ No newline at end of file