A gray-scale image processing library $Id: image.README.txt,v 2.4 2000/12/03 23:21:55 oleg Exp oleg $ ***** Platforms ***** Verification files ***** Highlights and idioms ---- Basic operations and image arithmetics ---- Pixel iterators ---- Image streams ---- Lazy images ---- Image filtration: convolutional, median, and morphological ---- Lookup table substitutions ---- Non-deterministic filtration ---- Reading/writing of TIFF files ***** Grand plans ***** Revision history ***** Comments/questions/problem reports/etc are all very welcome. Please send them to me at oleg-at-pobox.com or oleg-at-okmij.org or oleg-at-computer.org http://pobox.com/~oleg/ftp/ ***** Platforms I have personally compiled and tested the grayimage and C++ advanced iostream libraries on the following platforms: Sun Ultra-2/Solaris 2.6, gcc 2.95.2 i686/FreeBSD 4.0, gcc 2.95.2 i686/Linux 2.2.14, gcc 2.95.2 The previous (2.3) version also works on SunSparc20/Solaris 2.4, gcc 2.7.2, libg++ 2.7.1 SunSparc20/Solaris 2.3, SunPro C++ compiler HP 9000/{750,770,712}, HP/UX 9.0.5, 9.0.7 and 10.0, gcc 2.7.2, libg++ 2.7.1 PowerMac 7100/80, Metrowerk's CodeWarrior C++, v. 7 - 9 Intel, Windows95, Borland C++ 4.5 (the binaries then ran under Windows NT 4.0 beta) BeOS Preview Release I know that the packages also work on DEC Alpha, and Concurrent Maxion 8000/RTU 6.2V25 (all with gcc 2.7.2 compiler) ***** You'll need libcppadvio.a, an "advanced" C++ iostream classlib, to compile and use this grayimage library http://pobox.com/~oleg/ftp/packages.html#cpp.advio The latest version is 2.6, November 19, 2000. ***** Verification files: vimage vrectangle vimage_io vfilter vmorph_filter vfractal_map Don't forget to compile and run them, see comments in the Makefile for details. The verification code checks to see that all the functions in this package have compiled and run well. The validation code can also serve as an illustration of how package's classes and functions may be employed. ***** Highlights and idioms ---- Basic operations and image arithmetics Elementary pixel operations: assigning, subtracting, adding, etc. a value to all pixels, comparing every pixel with a value, assigning, adding, subtracting, comparing two images, computing image extrema, various norms and "scalar" products IMAGE im1(256,256,8); IMAGE im2 = zero(im1); im1 = 1; im2 = im1; to_every(im1) *= 4; assert( of_every(im1) == 4 ); assert( of_every(im1) > 0 ); im1.invert(); im1.clear(); to_every(im2) <<= 2; im1 = 5; to_every(im1) &= 0xfe; assert( im1==im2 ); im1.clear(); to_every(im1) -= of_every(im2); im2 = im1; assert( of_every(im1) * of_every(im1) == norm_2_sqr(im1) ); Accessing square or rectangular parts of an image without much fuss, and without moving a lot of stuff around im1 = 2*pattern; im2 = pattern; rowcol rightbottom(im1.q_nrows()-1,im1.q_ncols()-1); rowcol center(im1.q_nrows()/2,im1.q_ncols()/2); // Modifying the pixels only within // the lower left quadrant im1.rectangle(center,rightbottom) -= 2*pattern; assert( !(of_every(im1) == 2*pattern) && !(of_every(im1) != 2*pattern) ); Image i/o: reading and writing PGM, XWD and Group G (grayscale) TIFF 6.0 file formats with automatic recognition of the input image file format // Note the "extended" file name IMAGE raw_image = read_image("zcat ../pictures/lenna.tiff.Z |"); IMAGE image = raw_image.square_of(256,rowcol(120,100)); image.write_tiff("/tmp/ior","Original image"); image.display("Image to display"); image.write_pgm("| xv -"); // another way to display an image Squeezing/stretching and coercing images. Coercing means that one can assign one image to another no matter what their dimensions are. The source image would be shrunk or stretched to fit. Any dimension ratios are possible, absolutely any Test_image = pattern; IMAGE blown_out = expand_twice(Test_image); IMAGE blown_shrunk = shrink_twice(blown_out); assert( blown_shrunk == Test_image ); IMAGE shrunk(Test_image.q_nrows()/3+1, Test_image.q_ncols()/2,Test_image.q_depth()); shrunk.coerce(Test_image); IMAGE vert_stretched(Test_image.q_nrows()+7, Test_image.q_ncols(),Test_image.q_depth()); vert_stretched.coerce(Test_image); assert( vert_stretched.rectangle(rowcol(0,0), rowcol(Test_image.q_nrows()-1, Test_image.q_ncols()-1)) == Test_image ); Note that the last operation involves an implicit conversion from a rectangle to an image ---- Pixel iterators Advanced pixel operations via PixelActions are the most natural and efficient method of a "sweeping" image processing (that is, an operation that involves every pixel in a systematic way). Rather than writing a loop over all image rows and columns, you merely specify an action to be performed on a current pixel. The package would take care of the iteration, which is more efficient than a for() loop. The iteration walks through all pixels in a row-by-row fashion; you may take an advantage of this fact. PixelAction tells you the location of the pixel being accessed/modified, while PixelWiseAction does not. The latter is faster, of course. For example, the following snippet squares all image pixels and verifies the result: { IMAGE im = zero(Test_image); IMAGE im1 = zero(im); im = pattern; im.square_of(size,rowcol(0,0)) = -1; im1 = im; class SqrImage : public PixelWiseAction { void operator() (GRAY& pixel) { pixel = sqr((GRAY_SIGNED)pixel); } public: SqrImage(void) {} }; to_every(im1).apply(SqrImage()); assert( sum_over((Rectangle)im1) == norm_2_sqr(im) ); } BTW, lookup tables are better for that purpose (see below). In the next example, which makes a pin-striped image, the location of the current pixel _is_ important: class MakeVStripes : public PixelAction { GRAY pattern; void operation(GRAY& pixel, const card row, const card col) { pixel = col & 1 ? pattern : 0; } public: MakeVStripes(const GRAY _p) : pattern(_p) {} }; Test_image.apply(MakeVStripes(pattern)); Finally, the following snippet from the verification code checks out to see that pixel actions are indeed executed row-by-row. One iterator is used to assign to each pixel its own offset from the beginning of the image; the other iterator checks the assigned value. { cout << "Check to see that PixelAction are executed row-wise" << endl; class assign_pixels : public PixelAction { const card test_im_nrows, test_im_ncols; void operation(GRAY& pixel, const card row, const card col) { pixel = col + row*test_im_ncols; } public: assign_pixels(const IMAGE& im) : test_im_nrows(im.q_nrows()), test_im_ncols(im.q_ncols()) {} }; Test_image.apply(assign_pixels(Test_image)); class check_pixels : public ConstPixelAction { GRAY curr_value; void operation(const GRAY pixel, const card, const card) { assert(pixel == curr_value++); } public: check_pixels(void) : curr_value(0) {} }; Test_image.apply(check_pixels()); } ---- Image streams IMAGE "stream" is a _safe_ pixel pointer void write_pixmatrix(const IMAGE& image, EndianOut& file) { // Write the entire pixmatrix in one chunk Image_istream im_stream(image); while( !im_stream.eof() ) file.write_byte(im_stream.get()); } The entire iteration in the example above is inlined; it also requires less sanity checking overhead than a typical STL iterator. It has to be stressed that im_streams (like regular streams) are safe: even if you "forgot" to check for eof(), the program will not enter into an infinite loop or garble memory. One can well enlist regular iostreams for the same purpose (with a slightly higher overhead). Again, in the code above, there is only one check per iteration, and all operations on the im_istream are done inline. Thus the snippet runs just as fast as a "traditional" code, only safely. ---- Lazy images A Lazy image is an object that represents a "recipe" for making an image. In a situation where you need to pass an image object as a return value of a function, you should return a lazy image instead. The full image would be rolled out only when and where it is needed: IMAGE map = FractalMap(order); FractalMap is a *class*, not a simple function. However similar this looks to a returning of an object, it is _dramatically_ different. FractalMap() constructs a LazyImage, an object of just a few bytes long. A special "IMAGE(const LazyImage& recipe)" constructor follows the recipe and makes the fractal map right in place. No pixel is moved whatsoever! Since the FractalMap is a class, it can be subclassed to modify the default behavior. In the following example, the derived class overrides the default uniform noise generator with a Gaussian noise generator, which tends to produce better-looking clouds: class GaussNoise : public FractalMap { public: GaussNoise(const card order) : FractalMap(order) {} inline int get_noise(const card scale) const { long sum = 0; for(register int i=0; i<12; i++) sum += random(); // keep the result within return (scale * (sum-(6<<15)))>>17; } // [-scale/2,scale/2] }; IMAGE map(type == 0 ? FractalMap(order) : GaussNoise(order) ); This technique is particularly useful when one needs to construct an image in some particular way (e.g., by reading it from a file/database, or by decoding/decompressing) and return it. cout << "\tExpansion of the uniform image with a small stain\n"; Test_image = pattern; Test_image(0,0) = 1; Test_image(1,1) = 0; IMAGE blown_out = expand_twice(Test_image); class BlowImage : public LazyImage { const IMAGE& orig_image; void fill_in(IMAGE& im) const { for(register card i=0; i>=2; verify_identity(Test_image,vert_line); verify_identity(filter(Test_image). conv_col(conv_kernel(1,2,3,CommonDenom(6))),vert_line); verify_identity(filter(Test_image). conv(conv_kernel(0,2,0,over_2_up(1))),vert_line); // A truly 2D filtration.... Matrix3x3 kernel_matrix = {0,3,0, 0,4,0, 0,1,0}; verify_identity(filter(Test_image). conv(conv_kernel(kernel_matrix,over_2_up(3))),vert_line); expected = 0; expected.rectangle(rowcol(0,vert_line.q_ncols()/2-1), rowcol(vert_line.q_nrows()-1, vert_line.q_ncols()/2+1)) = -seed; verify_identity(filter(Test_image=vert_line). conv(conv_kernel(1,1,1),FilterIt::Rows),expected); ..... Matrix3x3 kernel_matrix1 = {1,2,3, 2,4,6, 3,6,9}; verify_identity(filter(Test_image=hor_line). conv(conv_kernel(kernel_matrix1,CommonDenom(36))), expected); The following (intentionally contrived) snippet from the verification code performs a phase shift of an image through filtration expected = small_sq(0,0); expected.square_of(2,rowcol(sq_row-1,sq_col-1)) = small_sq.square_of(2,rowcol(sq_row,sq_col)); verify_identity(filter(Test_image=small_sq). conv(conv_kernel(0,0,2,over_2_up(1))),expected); The package supports a median filtration with window sizes of three and five. The filtration can be done by rows only, by columns only, or by rows _and_ by columns. The latter is equivalent to a 2D median filtration with a diamond-shaped window. verify_identity(filter(Test_image).median(FilterIt::RowsAndColumns,3), expected); verify_identity(filter(Test_image).median(FilterIt::RowsAndColumns,5), expected); A FilterIt class (whose methods perform all the filtrations/convolutions/ substitutions above) is largely a syntactic sugar; you can't explicitly construct an object of this class, on stack or on heap. The only way the class can be used is within a pattern filter(image).median(RowsAndColumns); etc. Besides, this is the only way it makes sense. Thus, a filter() function constructs a _transient_ FilterIt object, so you can apply FilterIt methods to it to perform filtration/ lookup table substitutions, etc. Each of these methods, for example filter(image).translate(lookup,LookupT::CoerceFringes); returns a reference to the image after the operation (substitution, in the example above). So you can use the operation in chains like assert( of_every(filter(Test_image=diverse_image). translate(map,LookupT::LeaveFringes)) != seed ); ---- Lookup table substitutions Lookup table substitutions are very powerful and very fast. Indeed, in a typical 512x512 8-bit deep grayscale picture, there are 1/4 M pixels but only 256 possible pixel values. Therefore, if you want to make a new image as new_pixel(i,j) = f(old_pixel(i,j)) the fastest way of accomplishing this task is to create a look-up table, fill it in -- lookup(i) = f(i), i=0..255, -- and then do filter(image).translate(lookup,LookupT::CoerceFringes); to substitute all "old pixels" of an 'image' with the "new_pixel" values. The second argument, LookupT::CoerceFringes or LookupT::LeaveFringes determines what to do with pixels (if any) that fall outside of the lookup table range. Note, a lookup table can have any number of entries, even one. The following example changes all pixels with value 2 to value 3: LookupT map(LookupT::MapTo(2,3)); assert( of_every(filter(Test_image=diverse_image). translate(map,LookupT::LeaveFringes)) != 2 ); (the assert statement makes sure that there is no pixel with value 2 left in the translated image). a slight modification verify_pixel_value(filter(Test_image=diverse_image). translate(map,LookupT::CoerceFringes),3); makes all pixels have the same value, three. Aren't lookup tables powerful? There are many ways to create lookup tables: allocate a blank table and fill it in, create an Identity lookup table for a particular image depth and modify a few entries, or perform a composition of existing lookup tables. ---- Non-deterministic filtration One particular kind of filtration is a non-deterministic filtration. On the surface of it, it is a regular smoothing (blurring, averaging) with a 3x3 window. A catch is that some pixels may survive the averaging intact. The chance of pixel's survival depends on how much it sticks out of its neighborhood, and is controlled by a special parameter, temperature. A zero temperature means the averaging is carried out always; setting the temperature to 10*max_pixel_value will spare even the most outstanding pixel in 90% of the cases. // temperature 0 means the averaging is always // carried out verify_identity(filter(Test_image=vert_line).nondet_average(0),expected); // If the temperature > 0, some pixels are averaged, // but some do survive assert( !( of_every(filter(Test_image=vert_line).nondet_average(50)) == expected )); // some pixels of vert_line ought not change assert( !( of_every(Test_image) != -seed ) ); assert( !( of_every(Test_image) != -seed/3 ) );// but some pixels ought to Fractal clouds, FractalMap's, look especially good after being non-deterministically filtered. The non-deterministic averaging breaks creases so characteristic of fractal-generated images. The cloud will look rather realistic... ---- Reading/writing of TIFF files When reading a TIFF file, you can look up the value of a private or non-mandatory tag. Likewise, you can add any optional or private tag into a TIFF file you're about to create. Here is an example from a project to copy TIFF files with automatic compression/expansion of the pixel matrix, preserving all private tags that may occur. class MIFInfo: public MaybeCompressedStrips, public TIFFUserAction { char * tag_description; // Values of the corresponding tags int photometry; bool was_compressed; static const short pixmatrix_tags[]; // Pixmatrix-specific tags public: MIFInfo(EndianIn& _tiff_file); void operator() (TIFFBeingMadeDirectory& directory) const; bool q_was_compressed(void) const { return was_compressed; } bool q_pixmap_tag(const short tag) const; EndianIn& q_tiff_file(void) const { return tiff_file; } }; This is how you can look up a value of any tag // Take note of some specific info in a TIFF file // directory (just for convenience) MIFInfo::MIFInfo(EndianIn& _tiff_file) : MaybeCompressedStrips(_tiff_file) { photometry = look_up(TIFFTAG_PHOTOMETRIC,PHOTOMETRIC_MINISBLACK); assert( photometry == PHOTOMETRIC_MINISBLACK || photometry == PHOTOMETRIC_MINISWHITE ); if( (tag_description = read_str(TIFFTAG_IMAGEDESCRIPTION)) == 0 ) tag_description = strdup("no description"); was_compressed = look_up(TIFFTAG_COMPRESSION,COMPRESSION_NONE) != COMPRESSION_NONE; } And here is how new tags can be added to the tiff file under construction: the whole "trick" is to pass a TIFFUserAction object to an IMAGE::write_tiff() function // Clone all but pixmap specific // tags from the input TIFF file to the output one void MIFInfo::operator() (TIFFBeingMadeDirectory& directory) const { class tag_adder : public TIFFDirIter // Cloner-iteratee { const MIFInfo& mif_info; TIFFBeingMadeDirectory& directory; void operator() (const TIFFFileItem& item) const { if( mif_info.q_pixmap_tag(item.tag) ) return; // Don't clone a pixmap-specific tag directory += CloneTIFFDE::New(item,mif_info.q_tiff_file()); } public: tag_adder(const MIFInfo& _mif_info, TIFFBeingMadeDirectory& _directory) : mif_info(_mif_info), directory(_directory) {} }; for_each(tag_adder(*this,directory)); } class InputMIFImage : public LazyImage, public MIFInfo { void fill_in(IMAGE& im) const; public: InputMIFImage(EndianIn& file); }; void main(const int argc, const char * argv[]) { ..... EndianIn in_file(argv[1+noptions]); InputMIFImage mif_file(in_file); IMAGE image = mif_file; if( mif_file.q_was_compressed() ) { // The input image was compressed, image.write_tiff(output_file_name,"",mif_file); message("The input file is uncompressed into '%s' file\n", output_file_name); } .... } A paper presented at MacHack'96 talks in a little bit more detail about Lazy objects, iterators, image streams, etc. You can download the paper from http://pobox.com/~oleg/MacHack96/ ***** Grand plans - a PixelAction-like class for lookup table elements - in PixelAction, a method 'operation(GRAY& pixel)' should return bool (if it returns false, the traversal is terminated). Maybe there should be another class, like PixelControlledAction (which also would have specifications as to where to start and where/when to end the traversal), OR traversal classes for Rectangles; also, add q_nrows()/q_ncols() for rectangle. - DCT/Hadamar/etc transform of a rectangle? - make operation() method of PixelAction non-virtual, and make IMAGE::apply() templated to a PixelAction-like class (when gcc starts supporting member function templates) - templated IMAGE/LookUp/etc. member functions for_each(Action), for_each(Action,Another_image), etc; and find_first(). - add spans (by generalizing rowcol's), 1D, 2D spans, maybe even regions (non- rectangular spans). Add IMAGE constructors from spans. Use spans as generalized indices to take slices of an image - add a LazyMask to achieve B(A==5) = 4; (that is, for all i,j; if(A(i,j) ==5) B(i,j) = 4; ), like in Matlab. - simple color imagery with transformations among various tristimulus representations - support addition of a rectangle to an image or an image to a rectangle, or comparison between a rectangle and an image - add convolutions with a 5-point kernel - add STL-like "iterator" (accessor, actually) to access every pixel of an image (and also accessors that are limited to a row, a col, and iterators over the rectangle). Rectangle actually must be an iterator: for_each(Rect()) += ^=, etc. operations: which takes a stream, and applies a given operation to it. Thus, the whole design should be as follows: a data class that just holds a chunk of memory; an iterator over that class that provides a linear (sequential) access to the chunk; an interface class that provides access to some rows/columns, and an interface class that provides a truly random access (it's the only class that should have a column index, for a faster access) - Add routines to retrieve a pixel at a fractional position (and, performing interpolation). Useful in morphing/scaling. See Apple "develop" issue 10, "Drawing in GWorlds... Subpixel sampling" (p. 70). It can be used in a general image-mapping routine (say, to do image rotation by an arbitrary angle, etc) - use only integer arithmetics in IMAGE::coerce() (in Bresenham's spirit) - add a class IMAGETile to deal with a part of an image file *without* loading it entirely into memory. Show how to perform convolutions etc. operations one tile at a time. - LaplPyr: template of a template (template which is itself template) in pyramid_io. "banded" pyramid as a collection of 4 quadtrees intersperesed with each other; have "dad-kid" traversal of these "sub"trees - LaplPyr: Handle zerotrees in a different way: instead of marking LaplPyr nodes as being predicted, isolated zeros, etc, try to code vertices depth-first in such a model that a zero should raise the chance of all other vertices are zeros too. (or smaller compared to some threshold). - LaplPyr: predict -1 and 1, along with 0s; when unquantizing, unquantize 0 sometimes as 1/-1 (maybe depending on the predictions from above? or the neighbors?) - Add Displacement mapping: "Displacement mapping uses the brightness values of pixels in one image (the displacement map) to move pixels in another image (the target). Map pixels that are 100% white move the correspondent target pixels a user-specified amount in one direction (the positive offset), while map pixels that are 100% black move target pixels the same amount, but in the opposite direction (the negative offset). Map pixels that are 50% gray produce no offset, while pixels in other shades of gray produce offsets that are smaller than the user-specified amount. Positive offset values usually represent movement left and up; negative values usually represent movement right and down. Note the displacement map may be a color (that is, multi-channel) image as well. In that case, one channel controls vertical displacement, while another controls the vertical displacement". "Distorted Reality - the Magic of Displacement Mapping", Macworld, April 1998, p. 114. The paper gives several examples of using this rather versatile technique for producing many special effects. - Note http://www.inforamp.net/~poynton/ColorFAQ.html - Note Augural Image Zooming by Wm. Douglas Withers, pp. 48-58, DDJ, August 2000. Smart zooming: how to zoom images so they remain sharp. An extremely clever algorithm: basically a linear predictor based on the neighborhood. The parameters of the prediction are adaptive. The results are stunning. Augural Image Zooming is a part of a commercial package "PICTools ePIC" http://www.jpg.com/imagetech_jpeg.htm See the code on the DDJ site - Note an OpenCV library, www.intel.com/research/mrl/research/cvlib The library is very extensive, but the interface is terrible. I think that to_every(im1) += of_every(im2); is far better than iplAdd(im1,im2,im1); ***** Revision history version 2.4, December 2000 Glorified Image constructors are gone. They are subsumed by Lazy images (see the discussion above). Changed PixelAction and ConstPixelAction: they are now pure interfaces; the index of the current element is passed as an argument to the interface's 'operation' PixelPrimAction is replaced with PixelWiseAction/ PixelWiseConstAction, which are to be applied to a (resp, writable and const) PixelWise stream. IMAGE/scalar and pixel-wise IMAGE/IMAGE operations are implemented through dedicated PixelWise streams. Better const typing, better functionality Functions for norms of an image and of the difference between two images are no longer member functions of the IMAGE class. The norm functions no longer need a privileged access to an image. They are trivially implemented through appropriate ConstPixelWise operations. True IMAGE() copy-constructor. To allocate a blank image like another one, use "IMAGE a = zero(b);" Reading of an image is done through LazyImage. Therefore, the corresponding functions are no longer methods of the IMAGE class. Thus adding a function to read a new image format no longer requires any modification to the IMAGE interface. Tests (validation code) remained mostly unchanged. The only changes were insertions of to_every()/of_every() in a few places. This shows how much of the image interface remained the same. A better Makefile The code is embellished to make gcc 2.95.2 compiler happy. version 2.3, July 1996 Added Image_istream/Image_iostream, a few modules (read_pgm.cc) are changed to take advantage of these image streams A card data type is used throughout wherever image indices are concerned (which must be all non-negative) Reading/writing TIFF file have been greatly reworked: more elegant code; it's possible now for the user to read/write private tags Also, added some support for extra data types (SBYTE, FLOAT, etc) of TIFF 6.0 Added iterating over and cloning of TIFF IFD entries (useful for copying TIFF files with some modification) New, faster, algorithm for generating clouds, which also makes smoother clouds with a controlled crispness. FilterIt: made it a truly wrapper class, which cannot be explicitly constructed (and left hanging). This makes the design safer without inflicting any run-time penalty Added a non-deterministic, 3x3-averaging that may spare some pixels. The probability of pixel's survival depends on how much it sticks out, and on a 'temperature' Added a general 2D convolutional filtering with a 3x3 kernel version 2.0, July 1995 Makefile is much more user-friendly. Introduced card (typedef'ed as unsigned int) for row and col dimensions, indices and other quantities which are always non-negative. Added PixelAction and PixelPrimAction, classes to do a specific (maybe very complex) operation on every pixel regardless of its position (PixelPrimAction) or with regard to its position (PixelAction). The image is traversed row-by-row. Introduced LazyImage (which is what to return instead of IMAGE). bool is used when appropriate. Removed GNU extensions, code is made very portable. Sundry of optimizations. IMAGE::operator(): now it returns GRAY (the pixel itself) in const contexts, otherwise, it returns a reference to a pixel. read_xwd/read_pgm use "new style" with the PixelAction stuff. Median filtration re-worked; can be done now only by rows, only by cols, and both by Rows and columns. It's heavily optimized and must be very fast. Added convolutions: by rows, by cols, and separable 2D. Very efficient: kernel coeffs can be int, rational with power2 common denom, and rational. The three cases are handled separately (yet uniformly) and very efficiently (almost as much as one can get). Filtering is done "in-place" Added assignments with stretching/squeezing an image to fit (IMAGE::coerce()), the stretching/squeezing is done by arbitrary ratio! (the sizes of the original and the assigned image can be anything, not necessarily int). Added lookup table translations. version 1.15, Feb 8, 1995 Cosmetic changes to please gcc 2.6.3 and adjust to the new version of endian_io.h version 1.14, Mar 24, 1994 (previously posted on comp.sources.misc) Added support for reading/writing PGM and TIFF image file formats (in addition to the existed support for XWD format). Default write is in XWD format. Added image histogram equalization. Generalizing the Square_area class to the Rectangle class, which handles arbitrary rectangular areas of the image. Added comparison predicates that check a relation between an int and all pixels of the image. Added Abs() for putting down negative pixels, and application of a generic user-supplied function to every pixel of the image. Added +, -, *, >>, and << operations on rowcol (returning rowcol) Added assignments, comparison, and arithmetics (offsetting) on objects of the class rowcol. Added finding Extrema pixel values and image normalization for display. version 1.1, Apr 3, 1992 Initial revision