Panda3D
Loading...
Searching...
No Matches
imageTransformColors.cxx
Go to the documentation of this file.
1/**
2 * PANDA 3D SOFTWARE
3 * Copyright (c) Carnegie Mellon University. All rights reserved.
4 *
5 * All use of this software is subject to the terms of the revised BSD
6 * license. You should have received a copy of this license along
7 * with this source code in a file named "LICENSE."
8 *
9 * @file imageTransformColors.cxx
10 * @author drose
11 * @date 2009-03-25
12 */
13
15#include "string_utils.h"
16#include "pnmImage.h"
17#include <math.h>
18
19using std::max;
20using std::min;
21using std::string;
22
23/**
24 *
25 */
26ImageTransformColors::
27ImageTransformColors() {
28 set_program_brief("transform colors in an image file");
29 set_program_description
30 ("This program can apply a global color transform to all of the "
31 "pixels in an image, or in a series of images. This can be used, "
32 "for instance, to increase or decrease the dynamic range; or to "
33 "rotate the hue; or to reduce the saturation of colors in the image.\n\n"
34
35 "Each parameter is encoded in a 4x4 matrix, which modifies the R, G, B "
36 "colors of the image (the alpha values, if any, are not affected). "
37 "RGB values are clamped at 0 and 1 after the operation. "
38 "Multiple parameters are composed together in the order in which they "
39 "are listed.");
40
41 add_option
42 ("hls", "", 0,
43 "Specifies that all of the matrix operations are performed in HLS "
44 "space, instead of the default RGB space. In this mode, the first "
45 "component controls hue, the second controls lightness, and the third "
46 "controls saturation.",
47 &ImageTransformColors::dispatch_none, &_hls, nullptr);
48
49 add_option
50 ("range", "min,max", 0,
51 "Compresses the overall dynamic range from 0,1 to min,max. If min,max "
52 "exceed 0,1, the dynamic range is expanded. This doesn't make sense in "
53 "HLS mode.",
54 &ImageTransformColors::dispatch_range, nullptr, &_mat);
55
56 add_option
57 ("scale", "r,g,b", 0,
58 "Scales the r,g,b components by the indicated values. In HLS mode, "
59 "the scale is applied to the h,l,s components.",
60 &ImageTransformColors::dispatch_scale, nullptr, &_mat);
61
62 add_option
63 ("add", "r,g,b", 0,
64 "Adds the indicated values to the r,g,b components. In HLS mode, "
65 "the sum is applied to the h,l,s components.",
66 &ImageTransformColors::dispatch_add, nullptr, &_mat);
67
68 add_option
69 ("mat4", "m00,m01,m02,m03,m10,m11,m12,m13,m20,m21,m22,m23,m30,m31,m32,m33",
70 0, "Defines an arbitrary 4x4 RGB matrix.",
71 &ImageTransformColors::dispatch_mat4, nullptr, &_mat);
72
73 add_option
74 ("mat3", "m00,m01,m02,m10,m11,m12,m20,m21,m22", 0,
75 "Defines an arbitrary 3x3 RGB matrix.",
76 &ImageTransformColors::dispatch_mat3, nullptr, &_mat);
77
78 add_option
79 ("o", "filename", 50,
80 "Specify the filename to which the resulting image file will be written. "
81 "This is only valid when there is only one input image file on the command "
82 "line. If you want to process multiple files simultaneously, you must "
83 "use either -d or -inplace.",
84 &ImageTransformColors::dispatch_filename, &_got_output_filename, &_output_filename);
85
86 add_option
87 ("d", "dirname", 50,
88 "Specify the name of the directory in which to write the resulting image "
89 "files. If you are processing only one image file, this may be omitted "
90 "in lieu of the -o option. If you are processing multiple image files, "
91 "this may be omitted only if you specify -inplace instead.",
92 &ImageTransformColors::dispatch_filename, &_got_output_dirname, &_output_dirname);
93
94 add_option
95 ("inplace", "", 50,
96 "If this option is given, the input image files will be rewritten in "
97 "place with the results. This obviates the need to specify -d "
98 "for an output directory; however, it's risky because the original "
99 "input image files are lost.",
100 &ImageTransformColors::dispatch_none, &_inplace);
101
102 _mat = LMatrix4d::ident_mat();
103}
104
105/**
106 *
107 */
108void ImageTransformColors::
109run() {
110 _mat.write(nout, 0);
111 nout << "\n";
112
113 Filenames::iterator fi;
114 for (fi = _filenames.begin(); fi != _filenames.end(); ++fi) {
115 const Filename &source_filename = (*fi);
116 nout << source_filename << "\n";
117 PNMImage image;
118 if (!image.read(source_filename)) {
119 nout << "Couldn't read " << source_filename << "; ignoring.\n";
120 continue;
121 }
122
123 process_image(image);
124
125 Filename output_filename = get_output_filename(source_filename);
126 if (!image.write(output_filename)) {
127 nout << "Couldn't write " << output_filename << "; ignoring.\n";
128 }
129 }
130}
131
132/**
133 * Takes a series of 16 numbers as a 4x4 matrix.
134 */
135bool ImageTransformColors::
136dispatch_mat4(const string &opt, const string &arg, void *var) {
137 LMatrix4d &orig = *(LMatrix4d *)var;
138
139 vector_string words;
140 tokenize(arg, words, ",");
141
142 LMatrix4d mat;
143 bool okflag = false;
144 if (words.size() == 16) {
145 okflag =
146 string_to_double(words[0], mat[0][0]) &&
147 string_to_double(words[1], mat[0][1]) &&
148 string_to_double(words[2], mat[0][2]) &&
149 string_to_double(words[3], mat[0][3]) &&
150 string_to_double(words[4], mat[1][0]) &&
151 string_to_double(words[5], mat[1][1]) &&
152 string_to_double(words[6], mat[1][2]) &&
153 string_to_double(words[7], mat[1][3]) &&
154 string_to_double(words[8], mat[2][0]) &&
155 string_to_double(words[9], mat[2][1]) &&
156 string_to_double(words[10], mat[2][2]) &&
157 string_to_double(words[11], mat[2][3]) &&
158 string_to_double(words[12], mat[3][0]) &&
159 string_to_double(words[13], mat[3][1]) &&
160 string_to_double(words[14], mat[3][2]) &&
161 string_to_double(words[15], mat[3][3]);
162 }
163
164 if (!okflag) {
165 nout << "-" << opt
166 << " requires sixteen numbers separated by commas.\n";
167 return false;
168 }
169
170 orig *= mat;
171
172 return true;
173}
174
175/**
176 * Takes a series of 9 numbers as a 3x3 matrix.
177 */
178bool ImageTransformColors::
179dispatch_mat3(const string &opt, const string &arg, void *var) {
180 LMatrix4d &orig = *(LMatrix4d *)var;
181
182 vector_string words;
183 tokenize(arg, words, ",");
184
185 LMatrix3d mat;
186 bool okflag = false;
187 if (words.size() == 9) {
188 okflag =
189 string_to_double(words[0], mat[0][0]) &&
190 string_to_double(words[1], mat[0][1]) &&
191 string_to_double(words[2], mat[0][2]) &&
192 string_to_double(words[3], mat[1][0]) &&
193 string_to_double(words[4], mat[1][1]) &&
194 string_to_double(words[5], mat[1][2]) &&
195 string_to_double(words[6], mat[2][0]) &&
196 string_to_double(words[7], mat[2][1]) &&
197 string_to_double(words[8], mat[2][2]);
198 }
199
200 if (!okflag) {
201 nout << "-" << opt
202 << " requires nine numbers separated by commas.\n";
203 return false;
204 }
205
206 orig *= LMatrix4d(mat);
207
208 return true;
209}
210
211/**
212 * Takes a min,max dynamic range.
213 */
214bool ImageTransformColors::
215dispatch_range(const string &opt, const string &arg, void *var) {
216 LMatrix4d &orig = *(LMatrix4d *)var;
217
218 vector_string words;
219 tokenize(arg, words, ",");
220
221 double min, max;
222 bool okflag = false;
223 if (words.size() == 2) {
224 okflag =
225 string_to_double(words[0], min) &&
226 string_to_double(words[1], max);
227 }
228
229 if (!okflag) {
230 nout << "-" << opt
231 << " requires two numbers separated by commas.\n";
232 return false;
233 }
234
235 orig *= LMatrix4d::scale_mat(max - min) * LMatrix4d::translate_mat(min);
236
237 return true;
238}
239
240/**
241 * Accepts a componentwise scale.
242 */
243bool ImageTransformColors::
244dispatch_scale(const string &opt, const string &arg, void *var) {
245 LMatrix4d &orig = *(LMatrix4d *)var;
246
247 vector_string words;
248 tokenize(arg, words, ",");
249
250 double r, g, b;
251 bool okflag = false;
252 if (words.size() == 3) {
253 okflag =
254 string_to_double(words[0], r) &&
255 string_to_double(words[1], g) &&
256 string_to_double(words[2], b);
257 }
258
259 if (!okflag) {
260 nout << "-" << opt
261 << " requires three numbers separated by commas.\n";
262 return false;
263 }
264
265 orig *= LMatrix4d::scale_mat(r, g, b);
266
267 return true;
268}
269
270/**
271 * Accepts a componentwise add.
272 */
273bool ImageTransformColors::
274dispatch_add(const string &opt, const string &arg, void *var) {
275 LMatrix4d &orig = *(LMatrix4d *)var;
276
277 vector_string words;
278 tokenize(arg, words, ",");
279
280 double r, g, b;
281 bool okflag = false;
282 if (words.size() == 3) {
283 okflag =
284 string_to_double(words[0], r) &&
285 string_to_double(words[1], g) &&
286 string_to_double(words[2], b);
287 }
288
289 if (!okflag) {
290 nout << "-" << opt
291 << " requires three numbers separated by commas.\n";
292 return false;
293 }
294
295 orig *= LMatrix4d::translate_mat(r, g, b);
296
297 return true;
298}
299
300/**
301 * Does something with the additional arguments on the command line (after all
302 * the -options have been parsed). Returns true if the arguments are good,
303 * false otherwise.
304 */
305bool ImageTransformColors::
306handle_args(ProgramBase::Args &args) {
307 if (args.empty()) {
308 nout << "You must specify the image file(s) to read on the command line.\n";
309 return false;
310
311 } else {
312 // These only apply if we have specified any image files.
313 if (_got_output_filename && args.size() == 1) {
314 if (_got_output_dirname) {
315 nout << "Cannot specify both -o and -d.\n";
316 return false;
317 } else if (_inplace) {
318 nout << "Cannot specify both -o and -inplace.\n";
319 return false;
320 }
321
322 } else {
323 if (_got_output_filename) {
324 nout << "Cannot use -o when multiple image files are specified.\n";
325 return false;
326 }
327
328 if (_got_output_dirname && _inplace) {
329 nout << "Cannot specify both -inplace and -d.\n";
330 return false;
331
332 } else if (!_got_output_dirname && !_inplace) {
333 nout << "You must specify either -inplace or -d.\n";
334 return false;
335 }
336 }
337 }
338
339 Args::const_iterator ai;
340 for (ai = args.begin(); ai != args.end(); ++ai) {
341 Filename filename = (*ai);
342 if (!filename.exists()) {
343 nout << "Image file not found: " << filename << "\n";
344 return false;
345 }
346 _filenames.push_back(filename);
347 }
348
349 return true;
350}
351
352/**
353 * Returns the output filename of the egg file with the given input filename.
354 * This is based on the user's choice of -inplace, -o, or -d.
355 */
356Filename ImageTransformColors::
357get_output_filename(const Filename &source_filename) const {
358 if (_got_output_filename) {
359 nassertr(!_inplace && !_got_output_dirname && _filenames.size() == 1, Filename());
360 return _output_filename;
361
362 } else if (_got_output_dirname) {
363 nassertr(!_inplace, Filename());
364 Filename result = source_filename;
365 result.set_dirname(_output_dirname);
366 return result;
367 }
368
369 nassertr(_inplace, Filename());
370 return source_filename;
371}
372
373inline double
374hue2rgb(double m1, double m2, double h) {
375 h -= floor(h);
376 if (h < 1.0/6.0) {
377 return m1 + (m2 - m1) * h * 6.0;
378 }
379 if (h < 1.0/2.0) {
380 return m2;
381 }
382 if (h < 2.0/3.0) {
383 return m1 + (m2 - m1) * (2.0/3.0 - h) * 6;
384 }
385 return m1;
386}
387
388static LRGBColord
389hls2rgb(const LRGBColord &hls) {
390 double h = hls[0];
391 double l = max(min(hls[1], 1.0), 0.0);
392 double s = max(min(hls[2], 1.0), 0.0);
393
394 double m2;
395 if (l <= 0.5) {
396 m2 = l * (s + 1.0);
397 } else {
398 m2 = l + s - l * s;
399 }
400 double m1 = l * 2 - m2;
401
402 LRGBColord rgb(hue2rgb(m1, m2, h + 1.0/3.0),
403 hue2rgb(m1, m2, h),
404 hue2rgb(m1, m2, h - 1.0/3.0));
405 return rgb;
406}
407
408static LRGBColord
409rgb2hls(const LRGBColord &rgb) {
410 double r = rgb[0];
411 double g = rgb[1];
412 double b = rgb[2];
413 double h, l, s;
414
415 double minval = min(min(r, g), b);
416 double maxval = max(max(r, g), b);
417
418 double rnorm = 0.0, gnorm = 0.0, bnorm = 0.0;
419 double mdiff = maxval - minval;
420 double msum = maxval + minval;
421 l = 0.5 * msum;
422 if (maxval == minval) {
423 // Grayscale.
424 return LRGBColord(0.0, l, 0.0);
425 }
426
427 rnorm = (maxval - r) / mdiff;
428 gnorm = (maxval - g) / mdiff;
429 bnorm = (maxval - b) / mdiff;
430
431 if (l < 0.5) {
432 s = mdiff / msum;
433 } else {
434 s = mdiff / (2.0 - msum);
435 }
436
437 if (r == maxval) {
438 h = (6.0 + bnorm - gnorm) / 6.0;
439 } else if (g == maxval) {
440 h = (2.0 + rnorm - bnorm) / 6.0;
441 } else {
442 h = (4.0 + gnorm - rnorm) / 6.0;
443 }
444
445 if (h > 1.0) {
446 h -= 1.0;
447 }
448
449 return LRGBColord(h, l, s);
450}
451
452/**
453 * Processes a single image in-place.
454 */
455void ImageTransformColors::
456process_image(PNMImage &image) {
457 if (_hls) {
458 for (int yi = 0; yi < image.get_y_size(); ++yi) {
459 for (int xi = 0; xi < image.get_x_size(); ++xi) {
460 LRGBColord rgb = LCAST(double, image.get_xel(xi, yi));
461 rgb = hls2rgb(_mat.xform_point(rgb2hls(rgb)));
462 image.set_xel(xi, yi, LCAST(float, rgb));
463 }
464 }
465 } else {
466 for (int yi = 0; yi < image.get_y_size(); ++yi) {
467 for (int xi = 0; xi < image.get_x_size(); ++xi) {
468 LRGBColord rgb = LCAST(double, image.get_xel(xi, yi));
469 rgb = _mat.xform_point(rgb);
470 image.set_xel(xi, yi, LCAST(float, rgb));
471 }
472 }
473 }
474}
475
476int main(int argc, char *argv[]) {
478 prog.parse_command_line(argc, argv);
479 prog.run();
480 return 0;
481}
The name of a file, such as a texture file or an Egg file.
Definition filename.h:44
void set_dirname(const std::string &s)
Replaces the directory part of the filename.
Definition filename.cxx:704
bool exists() const
Returns true if the filename exists on the physical disk, false otherwise.
This program can apply a 4x4 color transform to all of the colors in the pixels of a series of images...
int get_x_size() const
Returns the number of pixels in the X direction.
int get_y_size() const
Returns the number of pixels in the Y direction.
The name of this class derives from the fact that we originally implemented it as a layer on top of t...
Definition pnmImage.h:58
LRGBColorf get_xel(int x, int y) const
Returns the RGB color at the indicated pixel.
Definition pnmImage.I:569
bool read(const Filename &filename, PNMFileType *type=nullptr, bool report_unknown_type=true)
Reads the indicated image filename.
Definition pnmImage.cxx:278
void set_xel(int x, int y, const LRGBColorf &value)
Changes the RGB color at the indicated pixel.
Definition pnmImage.I:579
bool write(const Filename &filename, PNMFileType *type=nullptr) const
Writes the image to the indicated filename.
Definition pnmImage.cxx:385
virtual void parse_command_line(int argc, char **argv)
Dispatches on each of the options on the command line, and passes the remaining parameters to handle_...
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
double string_to_double(const string &str, string &tail)
A string-interface wrapper around the C library strtol().
void tokenize(const string &str, vector_string &words, const string &delimiters, bool discard_repeated_delimiters)
Chops the source string up into pieces delimited by any of the characters specified in delimiters.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.