├── data ├── umn.jpg ├── dance.jpg ├── converted_umn.jpg └── converted_dance.jpg ├── README.md ├── test.py └── rgb_lab_formulation.py /data/umn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xahidbuffon/TF_RGB_LAB/HEAD/data/umn.jpg -------------------------------------------------------------------------------- /data/dance.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xahidbuffon/TF_RGB_LAB/HEAD/data/dance.jpg -------------------------------------------------------------------------------- /data/converted_umn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xahidbuffon/TF_RGB_LAB/HEAD/data/converted_umn.jpg -------------------------------------------------------------------------------- /data/converted_dance.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xahidbuffon/TF_RGB_LAB/HEAD/data/converted_dance.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### RGB-to-LAB and LAB-to-RGB color-space conversion 2 | - Tested on TensorFlow 1.x (Python 2.7) and TensorFlow 2.0 (Python 3.6) 3 | 4 | | Original image | RGB-to-LAB-to-RGB | 5 | |:--------------------|:---------------- 6 | | ![det-86](/data/dance.jpg) | ![det-106](/data/converted_dance.jpg) | 7 | | ![det-86](/data/umn.jpg) | ![det-106](/data/converted_umn.jpg) | 8 | 9 | 10 | ### Usage 11 | - Test script: [test.py](test.py) (use the tf_v1 = True/False for tf 1.x/2.0) 12 | - Color-space conversion libraries: [rgb_lab_formulation.py](rgb_lab_formulation.py) 13 | 14 | #### Acknowledgements for some functionalities 15 | - https://github.com/affinelayer/pix2pix-tensorflow/blob/master/pix2pix.py 16 | - https://github.com/torch/image/blob/9f65c30167b2048ecbe8b7befdc6b2d6d12baee9/generic/image.c 17 | - https://github.com/cameronfabbri/Colorizing-Images-Using-Adversarial-Networks 18 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | """ 2 | * Test script for color-space conversion (tf-1.x) 3 | - Maintainer: Jahid (email: islam034@umn.edu) 4 | - https://github.com/xahidbuffon/tf-rgb-lab 5 | """ 6 | import argparse 7 | import numpy as np 8 | import tensorflow as tf 9 | from matplotlib import pyplot as plt 10 | # local library 11 | import rgb_lab_formulation as Conv_img 12 | 13 | 14 | def test_tf_rgb_lab(img, tf_v1=True): 15 | # raw tensor 16 | raw_input = tf.image.convert_image_dtype(img, dtype=tf.float32) 17 | raw_input.set_shape([None, None, 3]) 18 | 19 | # convert to lab-space image {L, a, b} 20 | lab = Conv_img.rgb_to_lab(raw_input) 21 | L_chan, a_chan, b_chan = Conv_img.preprocess_lab(lab) 22 | lab = Conv_img.deprocess_lab(L_chan, a_chan, b_chan) 23 | 24 | # get back the RGB image (tensor) 25 | true_image = Conv_img.lab_to_rgb(lab) 26 | true_image = tf.image.convert_image_dtype(true_image, dtype=tf.uint8, saturate=True) 27 | 28 | # get image array from tensor 29 | if tf_v1: # for tf v1.x 30 | init_op = tf.global_variables_initializer() 31 | with tf.Session() as sess: 32 | sess.run(init_op) 33 | image = true_image.eval() 34 | else: # for tf v2.0 35 | tf.compat.v1.disable_eager_execution() 36 | init_op = tf.compat.v1.global_variables_initializer() 37 | with tf.compat.v1.Session() as sess: 38 | sess.run(init_op) 39 | image = true_image.numpy() 40 | # save/show image 41 | plt.imshow(image) 42 | plt.imsave('output.jpg', image) 43 | plt.show() 44 | 45 | 46 | if __name__ == '__main__': 47 | parser = argparse.ArgumentParser() 48 | parser.add_argument('--im_path', dest='im_path', type=str, default='data/umn.jpg') 49 | args = parser.parse_args() 50 | img = plt.imread(args.im_path) 51 | test_tf_rgb_lab(img, tf_v1=True) 52 | 53 | -------------------------------------------------------------------------------- /rgb_lab_formulation.py: -------------------------------------------------------------------------------- 1 | """ 2 | * Modules for and color-space conversion (tf-1.x) 3 | - Maintainer: Jahid (email: islam034@umn.edu) 4 | - https://github.com/xahidbuffon/tf-rgb-lab 5 | """ 6 | from __future__ import division 7 | import tensorflow as tf 8 | 9 | 10 | def preprocess(image): 11 | with tf.name_scope('preprocess'): 12 | # [0, 1] => [-1, 1] 13 | return image * 2 - 1 14 | 15 | def deprocess(image): 16 | with tf.name_scope('deprocess'): 17 | # [-1, 1] => [0, 1] 18 | return (image + 1) / 2 19 | 20 | def preprocess_lab(lab): 21 | with tf.name_scope('preprocess_lab'): 22 | L_chan, a_chan, b_chan = tf.unstack(lab, axis=2) 23 | # L_chan: black and white with input range [0, 100] 24 | # a_chan/b_chan: color channels with input range ~[-110, 110], not exact 25 | # [0, 100] => [-1, 1], ~[-110, 110] => [-1, 1] 26 | return [L_chan / 50 - 1, a_chan / 110, b_chan / 110] 27 | 28 | def deprocess_lab(L_chan, a_chan, b_chan): 29 | with tf.name_scope('deprocess_lab'): 30 | #TODO This is axis=3 instead of axis=2 when deprocessing batch of images 31 | # ( we process individual images but deprocess batches) 32 | #return tf.stack([(L_chan + 1) / 2 * 100, a_chan * 110, b_chan * 110], axis=3) 33 | return tf.stack([(L_chan + 1) / 2 * 100, a_chan * 110, b_chan * 110], axis=2) 34 | 35 | def augment(image, brightness): 36 | # (a, b) color channels, combine with L channel and convert to rgb 37 | a_chan, b_chan = tf.unstack(image, axis=3) 38 | L_chan = tf.squeeze(brightness, axis=3) 39 | lab = deprocess_lab(L_chan, a_chan, b_chan) 40 | rgb = lab_to_rgb(lab) 41 | return rgb 42 | 43 | def check_image(image): 44 | assertion = tf.assert_equal(tf.shape(image)[-1], 3, message='image must have 3 color channels') 45 | with tf.control_dependencies([assertion]): 46 | image = tf.identity(image) 47 | 48 | if image.get_shape().ndims not in (3, 4): 49 | raise ValueError('image must be either 3 or 4 dimensions') 50 | 51 | # make the last dimension 3 so that you can unstack the colors 52 | shape = list(image.get_shape()) 53 | shape[-1] = 3 54 | image.set_shape(shape) 55 | return image 56 | 57 | def rgb_to_lab(srgb): 58 | # based on https://github.com/torch/image/blob/9f65c30167b2048ecbe8b7befdc6b2d6d12baee9/generic/image.c 59 | with tf.name_scope('rgb_to_lab'): 60 | srgb = check_image(srgb) 61 | srgb_pixels = tf.reshape(srgb, [-1, 3]) 62 | with tf.name_scope('srgb_to_xyz'): 63 | linear_mask = tf.cast(srgb_pixels <= 0.04045, dtype=tf.float32) 64 | exponential_mask = tf.cast(srgb_pixels > 0.04045, dtype=tf.float32) 65 | rgb_pixels = (srgb_pixels / 12.92 * linear_mask) + (((srgb_pixels + 0.055) / 1.055) ** 2.4) * exponential_mask 66 | rgb_to_xyz = tf.constant([ 67 | # X Y Z 68 | [0.412453, 0.212671, 0.019334], # R 69 | [0.357580, 0.715160, 0.119193], # G 70 | [0.180423, 0.072169, 0.950227], # B 71 | ]) 72 | xyz_pixels = tf.matmul(rgb_pixels, rgb_to_xyz) 73 | 74 | # https://en.wikipedia.org/wiki/Lab_color_space#CIELAB-CIEXYZ_conversions 75 | with tf.name_scope('xyz_to_cielab'): 76 | # convert to fx = f(X/Xn), fy = f(Y/Yn), fz = f(Z/Zn) 77 | 78 | # normalize for D65 white point 79 | xyz_normalized_pixels = tf.multiply(xyz_pixels, [1/0.950456, 1.0, 1/1.088754]) 80 | 81 | epsilon = 6/29 82 | linear_mask = tf.cast(xyz_normalized_pixels <= (epsilon**3), dtype=tf.float32) 83 | exponential_mask = tf.cast(xyz_normalized_pixels > (epsilon**3), dtype=tf.float32) 84 | fxfyfz_pixels = (xyz_normalized_pixels / (3 * epsilon**2) + 4/29) * linear_mask + (xyz_normalized_pixels ** (1/3)) * exponential_mask 85 | 86 | # convert to lab 87 | fxfyfz_to_lab = tf.constant([ 88 | # l a b 89 | [ 0.0, 500.0, 0.0], # fx 90 | [116.0, -500.0, 200.0], # fy 91 | [ 0.0, 0.0, -200.0], # fz 92 | ]) 93 | lab_pixels = tf.matmul(fxfyfz_pixels, fxfyfz_to_lab) + tf.constant([-16.0, 0.0, 0.0]) 94 | 95 | return tf.reshape(lab_pixels, tf.shape(srgb)) 96 | 97 | 98 | def lab_to_rgb(lab): 99 | with tf.name_scope('lab_to_rgb'): 100 | lab = check_image(lab) 101 | lab_pixels = tf.reshape(lab, [-1, 3]) 102 | # https://en.wikipedia.org/wiki/Lab_color_space#CIELAB-CIEXYZ_conversions 103 | with tf.name_scope('cielab_to_xyz'): 104 | # convert to fxfyfz 105 | lab_to_fxfyfz = tf.constant([ 106 | # fx fy fz 107 | [1/116.0, 1/116.0, 1/116.0], # l 108 | [1/500.0, 0.0, 0.0], # a 109 | [ 0.0, 0.0, -1/200.0], # b 110 | ]) 111 | fxfyfz_pixels = tf.matmul(lab_pixels + tf.constant([16.0, 0.0, 0.0]), lab_to_fxfyfz) 112 | 113 | # convert to xyz 114 | epsilon = 6/29 115 | linear_mask = tf.cast(fxfyfz_pixels <= epsilon, dtype=tf.float32) 116 | exponential_mask = tf.cast(fxfyfz_pixels > epsilon, dtype=tf.float32) 117 | xyz_pixels = (3 * epsilon**2 * (fxfyfz_pixels - 4/29)) * linear_mask + (fxfyfz_pixels ** 3) * exponential_mask 118 | 119 | # denormalize for D65 white point 120 | xyz_pixels = tf.multiply(xyz_pixels, [0.950456, 1.0, 1.088754]) 121 | 122 | with tf.name_scope('xyz_to_srgb'): 123 | xyz_to_rgb = tf.constant([ 124 | # r g b 125 | [ 3.2404542, -0.9692660, 0.0556434], # x 126 | [-1.5371385, 1.8760108, -0.2040259], # y 127 | [-0.4985314, 0.0415560, 1.0572252], # z 128 | ]) 129 | rgb_pixels = tf.matmul(xyz_pixels, xyz_to_rgb) 130 | # avoid a slightly negative number messing up the conversion 131 | rgb_pixels = tf.clip_by_value(rgb_pixels, 0.0, 1.0) 132 | linear_mask = tf.cast(rgb_pixels <= 0.0031308, dtype=tf.float32) 133 | exponential_mask = tf.cast(rgb_pixels > 0.0031308, dtype=tf.float32) 134 | srgb_pixels = (rgb_pixels * 12.92 * linear_mask) + ((rgb_pixels ** (1/2.4) * 1.055) - 0.055) * exponential_mask 135 | 136 | return tf.reshape(srgb_pixels, tf.shape(lab)) 137 | --------------------------------------------------------------------------------