├── .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 | 
24 |
25 | $ python example.py --source images/woods.jpg --target images/storm.jpg
26 | 
27 |
28 | $ python example.py --source images/ocean_sunset.jpg --target images/ocean_day.jpg
29 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------