├── .gitignore ├── requeriments.txt ├── setup.cfg ├── images ├── autumn.jpg ├── storm.jpg ├── woods.jpg ├── ocean_day.jpg ├── fallingwater.jpg └── ocean_sunset.jpg ├── color_transfer ├── __init__.pyc └── __init__.py ├── docs └── images │ ├── woods_storm.png │ ├── sunset_ocean.png │ └── autumn_fallingwater.png ├── setup.py ├── LICENSE.txt ├── README.md └── example.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | -------------------------------------------------------------------------------- /requeriments.txt: -------------------------------------------------------------------------------- 1 | numpy==1.14.3 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /images/autumn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/images/autumn.jpg -------------------------------------------------------------------------------- /images/storm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/images/storm.jpg -------------------------------------------------------------------------------- /images/woods.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/images/woods.jpg -------------------------------------------------------------------------------- /images/ocean_day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/images/ocean_day.jpg -------------------------------------------------------------------------------- /images/fallingwater.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/images/fallingwater.jpg -------------------------------------------------------------------------------- /images/ocean_sunset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/images/ocean_sunset.jpg -------------------------------------------------------------------------------- /color_transfer/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/color_transfer/__init__.pyc -------------------------------------------------------------------------------- /docs/images/woods_storm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/docs/images/woods_storm.png -------------------------------------------------------------------------------- /docs/images/sunset_ocean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/docs/images/sunset_ocean.png -------------------------------------------------------------------------------- /docs/images/autumn_fallingwater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrosebr1/color_transfer/HEAD/docs/images/autumn_fallingwater.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name = 'color_transfer', 5 | packages = ['color_transfer'], 6 | version = '0.1', 7 | description = 'Implements color transfer between two images using the Lab color space, similar to the Reinhard et al. paper, "Color Transfer between Images"', 8 | author = 'Adrian Rosebrock', 9 | author_email = 'adrian@pyimagesearch.com', 10 | url = 'https://github.com/jrosebr1/color_transfer', 11 | download_url = 'https://github.com/jrosebr1/color_transfer/tarball/0.1', 12 | keywords = ['computer vision', 'image processing', 'color', 'rgb', 'lab'], 13 | classifiers = [], 14 | ) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Adrian Rosebrock, http://www.pyimagesearch.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Super fast color transfer between images 2 | ============== 3 | 4 | The color_transfer package is an OpenCV and Python implementation based (loosely) on [*Color Transfer between Images*](http://www.thegooch.org/Publications/PDFs/ColorTransfer.pdf) [Reinhard et al., 2001] The algorithm itself is extremely efficient (much faster than histogram based methods), requiring only the mean and standard deviation of pixel intensities for each channel in the L\*a\*b\* color space. 5 | 6 | For more information, along with a detailed code review, [take a look at this post on my blog](http://www.pyimagesearch.com/2014/06/30/super-fast-color-transfer-images/). 7 | 8 | #Requirements 9 | - OpenCV 10 | - NumPy 11 | 12 | #Install 13 | To install, make sure you have installed NumPy and compiled OpenCV with Python bindings enabled. 14 | 15 | From there, there easiest way to install is via pip: 16 | 17 | $ pip install color_transfer 18 | 19 | #Examples 20 | Below are some examples showing how to run the example.py demo and the associated color transfers between images. 21 | 22 | $ python example.py --source images/autumn.jpg --target images/fallingwater.jpg 23 | ![Autumn and Fallingwater screenshot](docs/images/autumn_fallingwater.png?raw=true) 24 | 25 | $ python example.py --source images/woods.jpg --target images/storm.jpg 26 | ![Woods and Storm screenshot](docs/images/woods_storm.png?raw=true) 27 | 28 | $ python example.py --source images/ocean_sunset.jpg --target images/ocean_day.jpg 29 | ![Sunset and Ocean screenshot](docs/images/sunset_ocean.png?raw=true) -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # USAGE 2 | # python example.py --source images/ocean_sunset.jpg --target images/ocean_day.jpg 3 | 4 | # import the necessary packages 5 | from color_transfer import color_transfer 6 | import numpy as np 7 | import argparse 8 | import cv2 9 | 10 | def show_image(title, image, width = 300): 11 | # resize the image to have a constant width, just to 12 | # make displaying the images take up less screen real 13 | # estate 14 | r = width / float(image.shape[1]) 15 | dim = (width, int(image.shape[0] * r)) 16 | resized = cv2.resize(image, dim, interpolation = cv2.INTER_AREA) 17 | 18 | # show the resized image 19 | cv2.imshow(title, resized) 20 | 21 | def str2bool(v): 22 | if v.lower() in ('yes', 'true', 't', 'y', '1'): 23 | return True 24 | elif v.lower() in ('no', 'false', 'f', 'n', '0'): 25 | return False 26 | else: 27 | raise argparse.ArgumentTypeError('Boolean value expected.') 28 | 29 | # construct the argument parser and parse the arguments 30 | ap = argparse.ArgumentParser() 31 | ap.add_argument("-s", "--source", required = True, 32 | help = "Path to the source image") 33 | ap.add_argument("-t", "--target", required = True, 34 | help = "Path to the target image") 35 | ap.add_argument("-c", "--clip", type = str2bool, default = 't', 36 | help = "Should np.clip scale L*a*b* values before final conversion to BGR? " 37 | "Approptiate min-max scaling used if False.") 38 | ap.add_argument("-p", "--preservePaper", type = str2bool, default = 't', 39 | help = "Should color transfer strictly follow methodology layed out in original paper?") 40 | ap.add_argument("-o", "--output", help = "Path to the output image (optional)") 41 | args = vars(ap.parse_args()) 42 | 43 | # load the images 44 | source = cv2.imread(args["source"]) 45 | target = cv2.imread(args["target"]) 46 | 47 | # transfer the color distribution from the source image 48 | # to the target image 49 | transfer = color_transfer(source, target, clip=args["clip"], preserve_paper=args["preservePaper"]) 50 | 51 | # check to see if the output image should be saved 52 | if args["output"] is not None: 53 | cv2.imwrite(args["output"], transfer) 54 | 55 | # show the images and wait for a key press 56 | show_image("Source", source) 57 | show_image("Target", target) 58 | show_image("Transfer", transfer) 59 | cv2.waitKey(0) -------------------------------------------------------------------------------- /color_transfer/__init__.py: -------------------------------------------------------------------------------- 1 | # import the necessary packages 2 | import numpy as np 3 | import cv2 4 | 5 | def color_transfer(source, target, clip=True, preserve_paper=True): 6 | """ 7 | Transfers the color distribution from the source to the target 8 | image using the mean and standard deviations of the L*a*b* 9 | color space. 10 | 11 | This implementation is (loosely) based on to the "Color Transfer 12 | between Images" paper by Reinhard et al., 2001. 13 | 14 | Parameters: 15 | ------- 16 | source: NumPy array 17 | OpenCV image in BGR color space (the source image) 18 | target: NumPy array 19 | OpenCV image in BGR color space (the target image) 20 | clip: Should components of L*a*b* image be scaled by np.clip before 21 | converting back to BGR color space? 22 | If False then components will be min-max scaled appropriately. 23 | Clipping will keep target image brightness truer to the input. 24 | Scaling will adjust image brightness to avoid washed out portions 25 | in the resulting color transfer that can be caused by clipping. 26 | preserve_paper: Should color transfer strictly follow methodology 27 | layed out in original paper? The method does not always produce 28 | aesthetically pleasing results. 29 | If False then L*a*b* components will scaled using the reciprocal of 30 | the scaling factor proposed in the paper. This method seems to produce 31 | more consistently aesthetically pleasing results 32 | 33 | Returns: 34 | ------- 35 | transfer: NumPy array 36 | OpenCV image (w, h, 3) NumPy array (uint8) 37 | """ 38 | # convert the images from the RGB to L*ab* color space, being 39 | # sure to utilizing the floating point data type (note: OpenCV 40 | # expects floats to be 32-bit, so use that instead of 64-bit) 41 | source = cv2.cvtColor(source, cv2.COLOR_BGR2LAB).astype("float32") 42 | target = cv2.cvtColor(target, cv2.COLOR_BGR2LAB).astype("float32") 43 | 44 | # compute color statistics for the source and target images 45 | (lMeanSrc, lStdSrc, aMeanSrc, aStdSrc, bMeanSrc, bStdSrc) = image_stats(source) 46 | (lMeanTar, lStdTar, aMeanTar, aStdTar, bMeanTar, bStdTar) = image_stats(target) 47 | 48 | # subtract the means from the target image 49 | (l, a, b) = cv2.split(target) 50 | l -= lMeanTar 51 | a -= aMeanTar 52 | b -= bMeanTar 53 | 54 | if preserve_paper: 55 | # scale by the standard deviations using paper proposed factor 56 | l = (lStdTar / lStdSrc) * l 57 | a = (aStdTar / aStdSrc) * a 58 | b = (bStdTar / bStdSrc) * b 59 | else: 60 | # scale by the standard deviations using reciprocal of paper proposed factor 61 | l = (lStdSrc / lStdTar) * l 62 | a = (aStdSrc / aStdTar) * a 63 | b = (bStdSrc / bStdTar) * b 64 | 65 | # add in the source mean 66 | l += lMeanSrc 67 | a += aMeanSrc 68 | b += bMeanSrc 69 | 70 | # clip/scale the pixel intensities to [0, 255] if they fall 71 | # outside this range 72 | l = _scale_array(l, clip=clip) 73 | a = _scale_array(a, clip=clip) 74 | b = _scale_array(b, clip=clip) 75 | 76 | # merge the channels together and convert back to the RGB color 77 | # space, being sure to utilize the 8-bit unsigned integer data 78 | # type 79 | transfer = cv2.merge([l, a, b]) 80 | transfer = cv2.cvtColor(transfer.astype("uint8"), cv2.COLOR_LAB2BGR) 81 | 82 | # return the color transferred image 83 | return transfer 84 | 85 | def image_stats(image): 86 | """ 87 | Parameters: 88 | ------- 89 | image: NumPy array 90 | OpenCV image in L*a*b* color space 91 | 92 | Returns: 93 | ------- 94 | Tuple of mean and standard deviations for the L*, a*, and b* 95 | channels, respectively 96 | """ 97 | # compute the mean and standard deviation of each channel 98 | (l, a, b) = cv2.split(image) 99 | (lMean, lStd) = (l.mean(), l.std()) 100 | (aMean, aStd) = (a.mean(), a.std()) 101 | (bMean, bStd) = (b.mean(), b.std()) 102 | 103 | # return the color statistics 104 | return (lMean, lStd, aMean, aStd, bMean, bStd) 105 | 106 | def _min_max_scale(arr, new_range=(0, 255)): 107 | """ 108 | Perform min-max scaling to a NumPy array 109 | 110 | Parameters: 111 | ------- 112 | arr: NumPy array to be scaled to [new_min, new_max] range 113 | new_range: tuple of form (min, max) specifying range of 114 | transformed array 115 | 116 | Returns: 117 | ------- 118 | NumPy array that has been scaled to be in 119 | [new_range[0], new_range[1]] range 120 | """ 121 | # get array's current min and max 122 | mn = arr.min() 123 | mx = arr.max() 124 | 125 | # check if scaling needs to be done to be in new_range 126 | if mn < new_range[0] or mx > new_range[1]: 127 | # perform min-max scaling 128 | scaled = (new_range[1] - new_range[0]) * (arr - mn) / (mx - mn) + new_range[0] 129 | else: 130 | # return array if already in range 131 | scaled = arr 132 | 133 | return scaled 134 | 135 | def _scale_array(arr, clip=True): 136 | """ 137 | Trim NumPy array values to be in [0, 255] range with option of 138 | clipping or scaling. 139 | 140 | Parameters: 141 | ------- 142 | arr: array to be trimmed to [0, 255] range 143 | clip: should array be scaled by np.clip? if False then input 144 | array will be min-max scaled to range 145 | [max([arr.min(), 0]), min([arr.max(), 255])] 146 | 147 | Returns: 148 | ------- 149 | NumPy array that has been scaled to be in [0, 255] range 150 | """ 151 | if clip: 152 | scaled = np.clip(arr, 0, 255) 153 | else: 154 | scale_range = (max([arr.min(), 0]), min([arr.max(), 255])) 155 | scaled = _min_max_scale(arr, new_range=scale_range) 156 | 157 | return scaled 158 | --------------------------------------------------------------------------------