Panda3D
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 
14 #include "imageTransformColors.h"
15 #include "string_utils.h"
16 #include "pnmImage.h"
17 #include <math.h>
18 
19 using std::max;
20 using std::min;
21 using std::string;
22 
23 /**
24  *
25  */
26 ImageTransformColors::
27 ImageTransformColors() {
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  */
108 void ImageTransformColors::
109 run() {
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  */
135 bool ImageTransformColors::
136 dispatch_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  */
178 bool ImageTransformColors::
179 dispatch_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  */
214 bool ImageTransformColors::
215 dispatch_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  */
243 bool ImageTransformColors::
244 dispatch_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  */
273 bool ImageTransformColors::
274 dispatch_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  */
305 bool ImageTransformColors::
306 handle_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  */
356 Filename ImageTransformColors::
357 get_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 
373 inline double
374 hue2rgb(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 
388 static LRGBColord
389 hls2rgb(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 
408 static LRGBColord
409 rgb2hls(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  */
455 void ImageTransformColors::
456 process_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 
476 int main(int argc, char *argv[]) {
478  prog.parse_command_line(argc, argv);
479  prog.run();
480  return 0;
481 }
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().
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
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
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_...
bool write(const Filename &filename, PNMFileType *type=nullptr) const
Writes the image to the indicated filename.
Definition: pnmImage.cxx:385
bool read(const Filename &filename, PNMFileType *type=nullptr, bool report_unknown_type=true)
Reads the indicated image filename.
Definition: pnmImage.cxx:278
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
int get_y_size() const
Returns the number of pixels in the Y direction.
int get_x_size() const
Returns the number of pixels in the X direction.
LRGBColorf get_xel(int x, int y) const
Returns the RGB color at the indicated pixel.
Definition: pnmImage.I:480
The name of a file, such as a texture file or an Egg file.
Definition: filename.h:39
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.
void set_dirname(const std::string &s)
Replaces the directory part of the filename.
Definition: filename.cxx:704
This program can apply a 4x4 color transform to all of the colors in the pixels of a series of images...
void set_xel(int x, int y, const LRGBColorf &value)
Changes the RGB color at the indicated pixel.
Definition: pnmImage.I:522
bool exists() const
Returns true if the filename exists on the disk, false otherwise.
Definition: filename.cxx:1267