PhotoDemon 5.4.1 provides fixes for several issues found in version 5.4 (released two days ago). If you have automatic updates enabled, you will automatically be notified up the update, or you can manually download it here.
5.4.1 fixes the following bugs:
FIXED: Some controls do not display text correctly on Windows XP
FIXED: Some dialogs load very slowly when a translation is active
FIXED: Some edge detection methods fail to initiate when a translation is active
FIXED: Left-hand toolbox text formatting looks poor when a translation is active
FIXED: Tooltips not showing for picture box objects when a translation is active
FIXED: When the last-used folder in the batch process tool contains many images (1000+), the batch tool loads slowly on subsequent uses
FIXED: Minor typographical errors in translation files
This update also includes a few minor modifications and additions:
MODIFIED: In the batch process tool, when a selected image is removed from the batch list, its preview is now erased to prevent confusion
ADDED: further optimizations to translation engine. Program performance should now be much better while translations are active.
ADDED: batch process tool now displays the number of images in the current batch list
PhotoDemon 5.4 is complete. New features include language support (German, French, and Dutch), a full-featured batch processing wizard, shadow/highlight correction, nine new distort tools, vignetting, median noise removal, JPEG and PNG optimization, and more. Download it here.
Highlight feature: support for multiple languages!
This is the biggest addition in version 5.4, and I can only claim partial credit for it. Primary credit goes to Frank Donckers, a fellow VB programmer who prototyped the initial translation engine for me. As if that isn’t incredible enough, Frank also supplied the translations for French, German, and Dutch (Flemish), so I owe him an enormous debt of gratitude. Thank you, Frank!
One of the neatest aspects of this feature is the ability to change the language at run-time via the Language menu. Unlike every program I have ever used, no restart is required. PhotoDemon will dynamically change the program’s entire language immediately, and if you change your mind, you can switch to any other language at any time.
I hope these three languages are only the beginning. If you speak a language other than English, please consider contributing a new PhotoDemon translation! No programming knowledge is required, and you will receive full credit for your work.Contact me for more details.
Nine new Distort-style tools
Add and remove lens distortion. Swirl. Ripple. Pinch and whirl. Waves. Kaleidoscope. Polar conversion (both directions). Figured glass (dents).
Vastly improved file format support
JPEGs now support automatic EXIF rotation on import, and a variety of options on export (Huffman table optimization, progressive scan, thumbnail embedding, specific subsampling). TIFFs support CMYK encoding and a number of compression schemes (none, PackBits, LZW, CCITT 3 and 4, zLib, and more). PNG exporting supports variable compression strength, interlacing, and background color chunk preservation. PPMs can be exported with RAW or ASCII encoding. BMP and TGA files now support RLE encoding. And for icons, animated GIFs, and multipage TIFFs, all images inside a file can now be loaded (instead of just the first one).
These format settings can be accessed from the Tools -> Options menu, and the new Batch Process tool also provides direct access.
Revamped standard tools, including Box Blur, Gaussian Blur, Smart Blur, and Unsharp Masking.
PhotoDemon is now a much better photo editor, thanks to the revamp of its core convolution filters. Larger tool dialogs make it easier to see the result of your actions. Better performance means real-time previews, even at enormous radii (up to 200px for all filters, plus 500px for box blur!). And all convolution algorithms now use specialized edge handling code to make sure every part of the image – from center to border – is handled correctly.
Also, the program’s Gaussian Blur is now a true gaussian blur. There are no shortcuts, no estimations, and it’s still fast enough to preview in real-time.
New advanced color tools, including Shadow/Midtone/Highlight adjustments, color balancing, and monochrome-to-grayscale recovery
New stylize tools, including Film Grain, Vignetting, Modern Art, Trace Contour, Film Noir, and Comic Book
Noise removal via Median Filtering
Automatic image cropping
New Batch Process Wizard
If I had to pick a personal “favorite” new feature in this release, it would be the brand-new batch processing wizard. This tool is a highlight of PhotoDemon’s emphasis on usability, and I researched more than a dozen other image batch processing tools while building it. I could be biased, but I believe PhotoDemon is now the best general-purpose image batch processor available on the web.
Drag-and-drop is now supported when building the list of images to be processed – not only from within the dialog, by dragging between list boxes, but also from Windows Explorer. Live previews make it much easier to find the images you want, while helpful instructions on the left-hand side expose some of the more nuanced functionality.
Page 2 is the barest page of the new wizard. The current version allows you to skip photo editing actions (if you want to just do a batch rename or format conversion, for example), or you can apply any recorded macro. In the next release, I will add a set of “one-click” presets for common actions, like resizing, or optimizing images for the web.
Page 3 asks you to choose an output format. If you want to retain original image formats, that’s cool too – PhotoDemon now supports this! Alternatively, you can select a single output format, with access to the program’s full range of detailed format settings. In the example above, you can see all the options available for JPEGs, including new support for optimization (lossless file size reduction), thumbnails, progressive encoding, and specific subsampling.
The final page asks you to select an output folder where PhotoDemon can save the processed images. New to this release is a wide range of renaming options – things like adding custom text to each filename, removing text from each filename, changing case, and replacing spaces with underscores for web-bound images. Additionally, original filenames can be retained, or PhotoDemon can just use ascending numbers.
So that’s the new batch wizard! I’d love feedback from power users, as there are a lot of moving parts to the batch tool, and while I have been very thorough in my own testing, it’s impossible to test every combination of variables. So if you find anything that doesn’t work, please let me know.
As is usual with each PhotoDemon update, a number of existing tools received redesigns or new features. Gamma correction now displays live gamma curves, and each color component (red, green, and blue) can be adjusted individually. Dilate and Erode use a new algorithm that’s significantly more optimized, meaning sizes up to 200px radius can be previewed in real-time. Monochrome conversion supports any two color (not just black and white), while the printing and histogram dialogs were completely overhauled to make them more user-friendly.
Universal color depth support at import and export time
PhotoDemon can now write 1, 4, 8, 24, and 32bpp variations of every supported file format. By default, when saving images, color depth detection is completely automated – the program will count the number of colors in an image and automatically select the most appropriate color depth for the output file. Alternatively, you can set a preference to manually specify color depth at save time. This also works for grayscale images; for example, the JPEG encoder will now detect grayscale images and write out 8bpp JPEGs accordingly. Alpha thresholding is also available when saving 32bpp images to 8bpp (e.g. PNG to GIF).
This feature was a nightmare to implement, as PhotoDemon supports a huge variety of file formats, and each one has a detailed list of color depths it does or does not support. Full support for transparency adds a whole other layer of complexity. But now that the feature is completely implemented and rigorously tested, I can’t imagine it any other way. Color depth is not something users should have to worry about, and automatic handling should be a feature of every photo editor (rather than pestering you for color depth every time you save… *cough* GIMP *cough*).
New feature: pngnq-s9 plugin for optimizing PNG files
At the request of a good friend, PhotoDemon now provides integrated support for the pngnq-s9 variety of the famous pngnq library. For the uninitiated, pngnq provides a way to reduce 32bpp PNG files to 8bpp while still preserving complex alpha channels, allowing for file size reductions of up to 75%. Pngnq provides superior results over other tools by using a neural network to reduce image colors, unlike the brute-force median cut algorithm used by software like pngquant. See here for a gallery of sample images if you’re curious.
Pngnq-s9 is a further improvement over stock pngnq, including cool features like YUV color space matching for better results, and the ability to preserve alpha values of 0 and 255. When saving 32bpp PNG files to 8bpp, PhotoDemon will now lean on pngnq-s9 to do the heavy lifting.
In the next version of PhotoDemon, pngnq-s9 support will be integrated into the batch process wizard as a new “optimize for web” option. For now, if you want to test out the feature, head to Tools -> Options -> Saving, and change the “set outgoing color depth” option to “ask me what color depth I want to use”. Then save a 32bpp PNG image to 8bpp and compare the file size.
New plugin manager and plugin downloader
Sometimes it makes sense for PhotoDemon to use an existing open-source project instead of me writing a new feature from scratch. These support libraries are included as “plugins”, and there are four of them in current version. Each one provides indispensable features (like scanner support) at a fraction of the cost involved to write such a feature from scratch.
Some of these plugins expose additional functionality, but it has always been a challenge for PhotoDemon to expose these additional features to the user. So the program now has a detailed plugin manager, where advanced users can change settings on a per-plugin basis, including activating or deactivating plugins as necessary. The manager also tracks availability and version numbers of each plugin.
Many canvas and interface improvements
Larger effect and tool previews. Persistent zoom-in/zoom-out buttons. Image URLs and files can now be directly pasted as new images. Improved drag/drop support, including drag/drop from common dialogs. New “Safe” save behavior to avoid overwriting original files. New Close All Images menu. New algorithms for auto-zoom when images are loaded, meaning much better results at all screen sizes. Tool and file panels can now be hidden. Higher-quality dynamic icons for the program, taskbar, child windows, and Recent Images list. Improved support for low screen resolutions.
Program-wide performance improvements
More aggressive memory management means lower resource usage. Program loading has been heavily streamlined, and now happens in less than a second on modern hardware. Image loading is much faster and more robust, including better support for damaged or incomplete image files.
More robust and comprehensive error handling
When loading multiple images, the program will now suppress warnings and failures (such as invalid files) until all images have been loaded. Many subclassing issues have been resolved – so no more surprise crashes! Overall this release should be extremely stable.
This release was a lot bigger than I’d like future releases to be. The biggest delay came from adding language support, as that affected every piece of text in every part of the program (nearly 10,000 words in total!). Now that language support is complete, I foresee future releases being much tidier and quicker.
A developer’s work is never done, and a roadmap for version 5.6 is already being worked on. Some features that didn’t make the cut for 5.4 – like improvements to the selection tool, or a “smart resize” option – were cut at the last minute, and they will be among the first features added to 5.6. The batch process wizard will see a number of additions, and I’d love to add some advanced multilanguage features, like a way for casual users to fix or adjust translations on-the-fly. I also think I’m finally ready to tackle the monumental task of writing a user manual… should be fun!
PhotoDemon 5.4 is nearing completion, and I need help testing it. Version 5.4 provides a bunch of new features, including French, German, and Dutch (Flemish) language support. If you can help translate PhotoDemon into another language, please let me know! The translation process is very simple, and it requires no programming experience or special software.
Version 5.4 also includes nine new distort tools, tons of new file format features including specialized PNG and JPEG optimization, improved memory management, a new plugin manager, real-time Gaussian, Smart, and Box blur tools with variable radius, a full Unsharp Mask tool, vignetting, median filtering, adding film grain, automatic cropping, contour tracing, a new Batch Wizard, redesigned tool interfaces, and more. Please download the beta and let me know if you find any bugs.
Remember – if you are an advanced user, you can always download the most recent development build of PhotoDemon’s source code from its GitHub page.
PhotoDemon is funded by donations from users like you. Please consider a small donation to fund development and to help me support my family. Even $1.00 helps. Thank you!
List of what’s new and improved in v5.4 (so far)
Official support for multiple languages. This is the biggest addition in version 5.4, and I can only claim partial credit for it. Primary credit goes to Frank Donckers, a fellow VB programmer and the one who prototyped the initial translation engine. Frank also supplied the translations for French, German, and Dutch (Flemish), so I owe him an enormous debt of gratitude.
Vastly improved file format support. JPEGs now support automatic EXIF rotation on import, and a variety of options on export (Huffman table optimization, progressive scan, thumbnail embedding, specific subsampling). TIFF exporting supports CMYK encoding and a number of compression schemes (none, PackBits, LZW, CCITT 3 and 4, zLib, and more). PNG exporting supports variable compression strength, interlacing, and background color chunk. PPM exporting supports RAW or ASCII encoding. BMP and TGA now support RLE encoding. For ICO files, all icons inside the file can now be loaded (instead of just the first one).
Nine new Distort-style tools. Add and remove lens distortion. Swirl. Ripple. Pinch and whirl. Waves. Kaleidoscope. Polar conversion (both directions). Figured glass (dents).
New and improved standard tools, including Box Blur, Gaussian Blur, Smart Blur, and Unsharp Masking. Each of these functions now supports variable radii (up to hundreds of pixels), and all have been heavily optimized. Gaussian Blur is the fastest VB-only true gaussian ever written. (Not a joke.)
Tons of new tools, including Film Grain, Color Balance, Vignetting, Autocrop, Median, Modern Art, Trace Contour, Shadow/Midtone/Highlight, Monochrome -> Grayscale conversion, Film Noir, and Comic Book. All tools include real-time previews. A number of existing tools received big updates as well – particularly Gamma Correction, Dilate, Erode, Monochrome Conversion, and Printing.
New Batch Process wizard. This replaces the old Batch Convert tool, which was an interface nightmare. The new tool supports a number of new features, including drag/drop support of batch lists, live image previews, and tons of file renaming options (prefix, suffix, case conversion, removing text, conversion of spaces to underscores for web).
Universal color depth support at import and export time. PhotoDemon can now write 1, 4, 8, 24, and 32bpp variations of every supported file format. Color depth detection is automatic at save time – the program will count the number of colors in an image and automatically save to the most appropriate color depth. Alternatively, you can set a preference to manually specify color depth at save time. This also works for grayscale images; for example, the JPEG encoder will now detect grayscale images and write out 8bpp JPEGs accordingly. Alpha thresholding is also available when saving 32bpp images to 8bpp (e.g. PNG to GIF).
New pngnq-s9 plugin for optimizing PNG files.Pngnq-s9 is an optimized and feature-rich variant of the original pngnq optimization library. Pngnq-s9 works by converting 32bpp PNG files to 8bpp with a heavily optimized palette, including support for variable alpha channels. File size savings of over 50% are common. See the Options -> Plugin Manager -> pngnq-s9 menu for a full list of tunable parameters.
New plugin manager and plugin downloader. Plugins can now be individually enabled/disabled, and missing plugins can be automatically downloaded. All plugin installation and activation/deactivation can be applied without a program restart.
Many canvas and interface improvements. Larger effect and tool previews. Persistent zoom-in/zoom-out buttons. Image URLs and files can now be directly pasted as new images. Improved drag/drop support, including drag/drop from common dialogs. New “Safe” save behavior to avoid overwriting original files. New Close All Images menu. New algorithms for auto-zoom when images are loaded, meaning much better results at all screen sizes. Tool and file panels can now be hidden. Higher-quality dynamic icons for the program, taskbar, child windows, and Recent Images list. Improved support for low screen resolutions.
Many performance improvements. More aggressive memory management means lower resource usage. Program loading has been heavily streamlined, and now happens in less than a second on modern hardware. Image loading is much faster and more robust, including better support for damaged or incomplete image files.
Much more robust and comprehensive error handling. When loading multiple images, the program will now suppress warnings and failures (such as invalid files) until all images have been loaded. Many subclassing issues have been resolved – so no more surprise crashes! Overall this release should be extremely stable.
Here is a list of known bugs with the current beta. These bugs will be fixed before the final release.
When a new language is selected, some text may not be translated. This is not a problem with the translation engine – it is a problem with the translation files, which are still being finalized. All text will be translated in the final release.
When using a language other than English, some text may overflow its boundaries or disappear off the page. This is a known problem that is still being worked on. All text – in any language – should fit properly in the final release.
Today I made an interesting discovery: my PhotoDemon release dates are slipping further and further apart.
PhotoDemon 4.3 – 10 August 2012
PhotoDemon 4.4 – 21 August 2012 (11 days later)
PhotoDemon 5.0 – 21 September 2012 (31 days later)
PhotoDemon 5.2 – 28 November 2012 (68 days later)
PhotoDemon 5.4 – …? (76 days and counting)
If I throw together a quick chart for the delay between releases, a clear trend emerges:
According to the chart, I still have a good for or five months available to work on the next release…
Actually, PhotoDemon 5.4 is rapidly nearing completion! My excuse for this latest delay is a good one, and I imagine you already inferred it from the title of the article… but in case you didn’t:
The next PhotoDemon release will include support for multiple languages, with French, German, and Dutch (Vlaams) officially supported at release.
Language support has been a monster to implement on account of the large amount of text in the project, not to mention classic VB’s utter lack of usable translation libraries. As a result, an entirely novel language engine was written from scratch, with (quite lovely) XML files being used to supply the actual translation data.
To give you an idea of the scope: as part of the translation process, I wrote a separate program whose sole purpose was to extract all necessary text from the PhotoDemon source code so that a “master” language file could be assembled. According to it, PhotoDemon contains almost 1400 unique phrases, with a total of more than 7800 words. Every one of those phrase occurrences needed code added to handle the actual text substitution, and then there’s the obvious challenge of the translation itself.
Fortunately, I didn’t tackle this project alone. An absolutely amazing contributor by the name of Frank Donckers contacted me with the initial prototype of the translation engine, and it is Frank who is supplying the French, German, and Dutch translation text. I could not have built this feature without him – so thank you, Frank!
At present, the translation engine is pretty much complete in terms of base functionality – all program text is translated on the fly, and the user is free to change languages at run-time at their leisure (no restart required!). The coding solution behind this is quite elegant, if I might say so, and users shouldn’t experience any noticeable performance hit while a translation is active. The only noticeable delay might be an additional second or two of start-up time on older machines, on account of the translation XML file being loaded and parsed. I’ve got some ideas for speeding up this process, however, so even that delay may disappear by release time.
I would love to include official support for additional languages. If you are fluent in a language other than English, please consider contributing a new translation!
No programming experience or specialized software is required to translate. I’ve got a master translation file ready to go – all that’s needed is to plug in the translated text in whatever language you might speak.
If you’re familiar with XML or HTML, you can see how simple the translation document is. Each phrase consists of original text and translated text, and that’s all there is to it. The lines in green are just comments added for convenience, in this case comments written by the language extraction tool. Translators can safely ignore these.
Before wrapping up, I should mention one rather large caveat with multiple language support – at present, only “ANSI” (Windows-1252) languages are supported. I apologize for this, but one of the problems with classic VB is its inability to handle unicode characters without major hacking – and by major hacking, I mean replacing every user interface aspect of the project, as well as every string library. If I ever port PhotoDemon to another programming language (something I often consider), Unicode support would be much more feasible, but I’m afraid it’s not on the present roadmap.
I need at least a couple more weeks to hammer out a few remaining issues with version 5.4, which leaves plenty of time for any ambitious translators out there to pitch in and contribute a new translation before release day. If it helps, note that your name will be visibly displayed as the translator in not only the language source file, but I’ll also add you to the special thanks section of the program, the PhotoDemon website, and the README file.
I can always be reached via this contact form – so that’s the place to go if you’re willing to help. Thank you in advance!
One of the new features in the development branch of my open-source photo editor is a simple tool for correcting lens distortion. I thought I’d share the algorithm I use, in case others find it useful. (There are very few useful examples of lens correction on the Internet – most articles simply refer to existing software packages, rather than explaining how the software works.)
Lens distortion is a complex beast, and a lot of approaches have been developed to deal with it. Some professional software packages address the problem by providing a comprehensive list of cameras and lenses – then the user just picks their equipment from the list, and the software applies a correction algorithm using a table of hard-coded values. This approach requires way more resources than a small developer like myself could handle, so I chose a simpler solution: a universal algorithm that allows the user to apply their own correction, with two tunable parameters for controlling the strength of the correction.
The key part of the algorithm is less than ten lines of code, so there’s not much work involved. The effect is also fast enough to preview in real-time.
Before sharing the algorithm, let me demonstrate its output. Here is a sample photo that suffers from typical spherical distortion:
Pay special attention to the lines on the floor and the glass panels on the right.
Here’s the same image, as corrected by the algorithm in this article:
My use of simple bilinear resampling blurs the output slightly; a more sophisticated resampling technique would produce clearer results.
A key feature of the algorithm is that it works at any aspect ratio – rectangular images, like the one above, are handled just fine, as are perfectly square images.
Anyway, here is the required code, as pseudocode:
strength as floating point >= 0. 0 = no change, high numbers equal stronger correction.
zoom as floating point >= 1. (1 = no change in zoom)
set halfWidth = imageWidth / 2
set halfHeight = imageHeight / 2
if strength = 0 then strength = 0.00001
set correctionRadius = squareroot(imageWidth ^ 2 + imageHeight ^ 2) / strength
for each pixel (x,y) in destinationImage
set newX = x - halfWidth
set newY = y - halfHeight
set distance = squareroot(newX ^ 2 + newY ^ 2)
set r = distance / correctionRadius
if r = 0 then
set theta = 1
set theta = arctangent(r) / r
set sourceX = halfWidth + theta * newX * zoom
set sourceY = halfHeight + theta * newY * zoom
set color of pixel (x, y) to color of source image pixel at (sourceX, sourceY)
That’s all there is to it. Note that you’ll need to do some bounds checking, as sourceX and sourceY may lie outside the bounds of the original image. Note also that sourceX and sourceY will be floating-point values – so for best results, you’ll want to interpolate the color used instead of just clamping sourceX and sourceY to integer values.
I should mention that the algorithm works just fine without the zoom parameter. I added the zoom parameter after some experimentation; specifically, I find zoom useful in two ways:
On images with only minor lens distortion, zooming out reduces stretching artifacts at the edges of the corrected image
On images with severe distortion, such as true fish-eye photos, zooming-out retains more of the source material
If we attempt to correct the image without applying any zoom, the image must be stretched so far that much of the edges are lost completely:
By utilizing a zoom parameter, it is possible to include more of the image in the finished result:
Again, I only use a simple resampling technique; a more sophisticated one would produce clearer results at the edges.
If you’d like to see my actual source code, check out this GitHub link. The fun begins at line 194. I also include an optional radius parameter, which allows the user to correct only a subset of the image (rather than the entire thing), but other than that the code is identical to what you see above.
A good friend recently sent me a number of resources related to hooking common dialog controls. I’ve been interested in common dialog hooking for PhotoDemon, as it would allow me to add support for image previewing right in the dialog itself. This isn’t as necessary in modern versions of Windows (7 in particular includes a number of GDI+ improvements, making Explorer very robust with standard formats), but it can be helpful for unsupported formats like RAW photographs.
Unfortunately, several days of research have shown that it is not possible to hook a Vista or Windows 7 style dialog and maintain the modern layout. Let me explain with pictures:
The common dialog above comes directly from PhotoDemon’s current common dialog implementation. This is the native common dialog control on Vista and 7. I very much like it. As a comparison, here is the same dialog in Windows XP:
I strongly prefer the Vista/7-style dialog, particularly the breadcrumb nav and the persistent folder tree on the left.
Unfortunately, it is impossible to hook the Vista/7 dialog and maintain the native Vista/7 appearance. If you attempt to hook it, Windows will ALWAYS drop back to a previous generation common dialog. Here are some images to demonstrate, using a basic image preview hook:
Note that the image above uses an older version of the OPENFILENAME struct, namely (this is its declaration in VB):
Private Type OPENFILENAME
lStructSize As Long
hwndOwner As Long
hInstance As Long
lpstrFilter As String
lpstrCustomFilter As String
nMaxCustFilter As Long
nFilterIndex As Long
lpstrFile As String
nMaxFile As Long
lpstrFileTitle As String
nMaxFileTitle As Long
lpstrInitialDir As String
lpstrTitle As String
Flags As Long
nFileOffset As Integer
nFileExtension As Integer
lpstrDefExt As String
lCustData As Long
lpfnHook As Long
lpTemplateName As String
If you modify the struct to its newest version (as described here), you can slightly improve the dialog to look like this:
That is the best you can get if you want to hook a common dialog in Vista or 7.
The reason for this is that MS completely re-organised the file dialogs for Vista. Hooks are used to extend a file dialog by supplying a resource file. This gives the customiser too much power. They can all too easily modify standard elements of the dialog and indeed many apps did so. The reorganisation of the dialogs would have broken many apps that used hooks. Those would have tried to manipulate elements of the dialog that were not there, or were implemented differently. Legacy versions of the dialogs remain for such apps to “get their hooks into”.
You are correct that it is impossible to get the new look when you use a hook. Instead you need to use the IFileDialogCustomize interface to customise the dialog. This is less powerful but does result in appearance and behaviour that is more consistent with the standard part of the dialog.
(More information is available here for those who are interested.)
The take home message of all this is – if you work in classic VB and you want to hook a common dialog, you need to be content with an XP-style dialog. There is currently no way to maintain a Vista/7 style dialog while hooking.
For this reason, I’m going to stick with the stock common dialog control in PhotoDemon. I may look at a dedicated “browse” window in the future, which would allow for full image previewing, but I’m afraid such a feature is not on the roadmap for the next few versions.
All hooking-related screenshots were created using Carles PV’s iBMP project, which made it very easy to modify various hooking parameters and test the output. Thanks, Carles!
Today’s graphics programming topic – dithering – is one I receive a lot of emails about, which some may find surprising. You might think that dithering is something programmers shouldn’t have to deal with in 2012. Doesn’t dithering belong in the annals of technology history, a relic of times when “16 million color displays” were something programmers and users could only dream of? In an age when cheap mobile phones operate in full 32bpp glory, why am I writing an article about dithering?
Actually, dithering is still a surprisingly applicable technique, not just for practical reasons (such as preparing a full-color image for output on a non-color printer), but for artistic reasons as well. Dithering also has applications in web design, where it is a useful technique for reducing images with high color counts to lower color counts, reducing file size (and bandwidth) without harming quality. It also has uses when reducing 48 or 64bpp RAW-format digital photos to 24bpp RGB for editing.
And these are just image dithering uses – dithering still has extremely crucial roles to play in audio, but I’m afraid I won’t be discussing audio dithering here. Just image dithering.
In this article, I’m going to focus on three things:
a basic discussion of how image dithering works
eleven specific two-dimensional dithering formulas, including famous ones like “Floyd-Steinberg”
how to write a general-purpose dithering engine
Update 11 June 2016: some of the sample images in this article have been updated to better reflect the various dithering algorithms. Thank you to commenters who noted problems with the previous images!
On a modern LCD or LED screen – be it your computer monitor, smartphone, or TV – this full-color image can be displayed without any problems. But consider an older PC, one that only supports a limited palette. If we attempt to display the image on such a PC, it might look something like this:
Pretty nasty, isn’t it? Consider an even more dramatic example, where we want to print the cube image on a black-and-white printer. Then we’re left with something like this:
Problems arise any time an image is displayed on a device that supports less colors than the image contains. Subtle gradients in the original image may be replaced with blobs of uniform color, and depending on the restrictions of the device, the original image may become unrecognizable.
Dithering is an attempt to solve this problem. Dithering works by approximating unavailable colors with available colors, by mixing and matching available colors in a way that mimicks unavailable ones. As an example, here is the cube image once again reduced to the colors of a theoretical old PC – only this time, dithering has been applied:
If you look closely, you can see that this image uses the same colors as its non-dithered counterpart – but those few colors are arranged in a way that makes it seem like many more colors are present.
As another example, here is a black-and-white version of the image with similar dithering applied:
Despite only black and white being used, we can still make out the shape of the cube, right down to the hearts on either side. Dithering is an extremely powerful technique, and it can be used in ANY situation where data has to be represented at a lower resolution than it was originally created for. This article will focus specifically on images, but the same techniques can be applied to any 2-dimensional data (or 1-dimensional data, which is even simpler!).
The Basic Concept Behind Dithering
Boiled down to its simplest form, dithering is fundamentally about error diffusion.
Error diffusion works as follows: let’s pretend to reduce a grayscale photograph to black and white, so we can print it on a printer that only supports pure black (ink) or pure white (no ink). The first pixel in the image is dark gray, with a value of 96 on a scale from 0 to 255, with zero being pure black and 255 being pure white.
When converting such a pixel to black or white, we use a simple formula – is the color value closer to 0 (black) or 255 (white)? 96 is closer to 0 than to 255, so we make the pixel black.
At this point, a standard approach would simply move to the next pixel and perform the same comparison. But a problem arises if we have a bunch of “96 gray” pixels – they all get turned to black, and we’re left with a huge chunk of empty black pixels, which doesn’t represent the original gray color very well at all.
Error diffusion takes a smarter approach to the problem. As you might have inferred, error diffusion works by “diffusing” – or spreading – the error of each calculation to neighboring pixels. If it finds a pixel of 96 gray, it too determines that 96 is closer to 0 than to 255 – and so it makes the pixel black. But then the algorithm makes note of the “error” in its conversion – specifically, that the gray pixel we have forced to black was actually 96 steps away from black.
When it moves to the next pixel, the error diffusion algorithm adds the error of the previous pixel to the current pixel. If the next pixel is also 96 gray, instead of simply forcing that to black as well, the algorithm adds the error of 96 from the previous pixel. This results in a value of 192, which is actually closer to 255 – and thus closer to white! So it makes this particular pixel white, and it again makes note of the error – in this case, the error is -63, because 192 is 63 less than 255, which is the value this pixel was forced to.
As the algorithm proceeds, the “diffused error” results in an alternating pattern of black and white pixels, which does a pretty good job of mimicking the “96 gray” of the section – much better just forcing the color to black over and over again. Typically, when we finish processing a line of the image, we discard the error value we’ve been tracking and start over again at an error of “0” with the next line of the image.
Here is an example of the cube image from above with this exact algorithm applied – specifically, each pixel is converted to black or white, the error of the conversion is noted, and it is passed to the next pixel on the right:
Unfortunately, error diffusion dithering has problems of its own. For better or worse, dithering always leads to a spotted or stippled appearance. This is an inevitable side-effect of working with a small number of available colors – those colors are going to be repeated over and over again, because there are only so many of them.
In the simple error diffusion example above, another problem is evident – if you have a block of very similar colors, and you only push the error to the right, all the “dots” end up in the same place! This leads to funny lines of dots, which is nearly as distracting as the original, non-dithered version.
The problem is that we’re only using a one-dimensional error diffusion. By only pushing the error in one direction (right), we don’t distribute it very well. Since an image has two dimensions – horizontal and vertical – why not push the error in multiple directions? This will spread it out more evenly, which in turn will avoid the funny “lines of speckles” seen in the error diffusion example above.
Two-Dimensional Error Diffusion Dithering
There are many ways to diffuse an error in two dimensions. For example, we can spread the error to one or more pixels on the right, one or more pixels on the left, one or more pixels up, and one or more pixels down.
For simplicity of computation, all standard dithering formulas push the error forward, never backward. If you loop through an image one pixel at a time, starting at the top-left and moving right, you never want to push errors backward (e.g. left and/or up). The reason for this is obvious – if you push the error backward, you have to revisit pixels you’ve already processed, which leads to more errors being pushed backward, and you end up with an infinite cycle of error diffusion.
So for standard loop behavior (starting at the top-left of the image and moving right), we only want to push pixels right and down.
As for how specifically to propagate the error, a great number of individuals smarter than I have tackled this problem head-on. Let me share their formulas with you.
(Note: these dithering formulas are available multiple places online, but the best, most comprehensive reference I have found is this one.)
The first – and arguably most famous – 2D error diffusion formula was published by Robert Floyd and Louis Steinberg in 1976. It diffuses errors in the following pattern:
3 5 1
In the notation above, “X” refers to the current pixel. The fraction at the bottom represents the divisor for the error. Said another way, the Floyd-Steinberg formula could be written as:
3/16 5/16 1/16
But that notation is long and messy, so I’ll stick with the original.
To use our original example of converting a pixel of value “96” to 0 (black) or 255 (white), if we force the pixel to black, the resulting error is 96. We then propagate that error to the surrounding pixels by dividing 96 by 16 ( = 6), then multiplying it by the appropriate values, e.g.:
+18 +30 +6
By spreading the error to multiple pixels, each with a different value, we minimize any distracting bands of speckles like the original error diffusion example. Here is the cube image with Floyd-Steinberg dithering applied:
Not bad, eh?
Floyd-Steinberg dithering is easily the most well-known error diffusion algorithm. It provides reasonably good quality, while only requiring a single forward array (a one-dimensional array the width of the image, which stores the error values pushed to the next row). Additionally, because its divisor is 16, bit-shifting can be used in place of division – making it quite fast, even on old hardware.
As for the 1/3/5/7 values used to distribute the error – those were chosen specifically because they create an even checkerboard pattern for perfectly gray images. Clever!
One warning regarding “Floyd-Steinberg” dithering – some software may use other, simpler dithering formulas and call them “Floyd-Steinberg”, hoping people won’t know the difference. This excellent dithering article describes one such “False Floyd-Steinberg” algorithm:
This simplification of the original Floyd-Steinberg algorithm not only produces markedly worse output – but it does so without any conceivable advantage in terms of speed (or memory, as a forward-array to store error values for the next line is still required).
But if you’re curious, here’s the cube image after a “False Floyd-Steinberg” application:
Jarvis, Judice, and Ninke Dithering
In the same year that Floyd and Steinberg published their famous dithering algorithm, a lesser-known – but much more powerful – algorithm was also published. The Jarvis, Judice, and Ninke filter is significantly more complex than Floyd-Steinberg:
X 7 5
3 5 7 5 3
1 3 5 3 1
With this algorithm, the error is distributed to three times as many pixels as in Floyd-Steinberg, leading to much smoother – and more subtle – output. Unfortunately, the divisor of 48 is not a power of two, so bit-shifting can no longer be used – but only values of 1/48, 3/48, 5/48, and 7/48 are used, so these values can each be calculated but once, then propagated multiple times for a small speed gain.
Another downside of the JJN filter is that it pushes the error down not just one row, but two rows. This means we have to keep two forward arrays – one for the next row, and another for the row after that. This was a problem at the time the algorithm was first published, but on modern PCs or smartphones this extra requirement makes no difference. Frankly, you may be better off using a single error array the size of the image, rather than erasing the two single-row arrays over and over again.
Five years after Jarvis, Judice, and Ninke published their dithering formula, Peter Stucki published an adjusted version of it, with slight changes made to improve processing time:
X 8 4
2 4 8 4 2
1 2 4 2 1
The divisor of 42 is still not a power of two, but all the error propagation values are – so once the error is divided by 42, bit-shifting can be used to derive the specific values to propagate.
For most images, there will be minimal difference between the output of Stucki and JJN algorithms, so Stucki is often used because of its slight speed increase.
During the mid-1980’s, dithering became increasingly popular as computer hardware advanced to support more powerful video drivers and displays. One of the best dithering algorithms from this era was developed by Bill Atkinson, a Apple employee who worked on everything from MacPaint (which he wrote from scratch for the original Macintosh) to HyperCard and QuickDraw.
Atkinson’s formula is a bit different from others in this list, because it only propagates a fraction of the error instead of the full amount. This technique is sometimes offered by modern graphics applications as a “reduced color bleed” option. By only propagating part of the error, speckling is reduced, but contiguous dark or bright sections of an image may become washed out.
X 1 1
1 1 1
Seven years after Stucki published his improvement to Jarvis, Judice, Ninke dithering, Daniel Burkes suggested a further improvement:
X 8 4
2 4 8 4 2
Burkes’s suggestion was to drop the bottom row of Stucki’s matrix. Not only did this remove the need for two forward arrays, but it also resulted in a divisor that was once again a multiple of 2. This change meant that all math involved in the error calculation could be accomplished by simple bit-shifting, with only a minor hit to quality.
The final three dithering algorithms come from Frankie Sierra, who published the following matrices in 1989 and 1990:
X 5 3
2 4 5 4 2
2 3 2
X 4 3
1 2 3 2 1
These three filters are commonly referred to as “Sierra”, “Two-Row Sierra”, and “Sierra Lite”. Their output on the sample cube image is as follows:
Other dithering considerations
If you compare the images above to the dithering results of another program, you may find slight differences. This is to be expected. There are a surprising number of variables that can affect the precise output of a dithering algorithm, including:
Integer or floating point tracking of errors. Integer-only methods lose some resolution due to quantization errors.
Color bleed reduction. Some software reduces the error by a set value – maybe 50% or 75% – to reduce the amount of “bleed” to neighboring pixels.
The threshold cut-off for black or white. 127 or 128 are common, but on some images it may be helpful to use other values.
For color images, how luminance is calculated can make a big difference. I use the HSL luminance formula ( [max(R,G,B) + min(R,G,B)] / 2). Others use ([r+g+b] / 3) or one of the ITU formulas. YUV or CIELAB will offer even better results.
Gamma correction or other pre-processing modifications. It is often beneficial to normalize an image before converting it to black and white, and whichever technique you use for this will obviously affect the output.
Loop direction. I’ve discussed a standard “left-to-right, top-to-bottom” approach, but some clever dithering algorithms will follow a serpentine path, where left-to-right directionality is reversed each line. This can reduce spots of uniform speckling and give a more varied appearance, but it’s more complicated to implement.
For the demonstration images in this article, I have not performed any pre-processing to the original image. All color matching is done in the RGB space with a cut-off of 127 (values <= 127 are set to 0). Loop direction is standard left-to-right, top-to-bottom.
Which specific techniques you may want to use will vary according to your programming language, processing constraints, and desired output.
I count 9 algorithms, but you promised 11! Where are the other two?
So far I’ve focused purely on error-diffusion dithering, because it offers better results than static, non-diffusion dithering.
But for sake of completeness, here are demonstrations of two standard “ordered dither” techniques. Ordered dithering leads to far more speckling (and worse results) than error-diffusion dithering, but they require no forward arrays and are very fast to apply. For more information on ordered dithering, check out the relevant Wikipedia article.
With these, the article has now covered a total of 11 different dithering algorithms.
Writing your own general-purpose dithering algorithm
Earlier this year, I wrote a fully functional, general-purpose dithering engine for PhotoDemon (an open-source photo editor). Rather than post the entirety of the code here, let me refer you to the relevant page on GitHub. The black and white conversion engine starts at line 350. If you have any questions about the code – which covers all the algorithms described on this page – please let me know and I’ll post additional explanations.
That engine works by allowing you to specify any dithering matrix in advance, just like the ones on this page. Then you hand that matrix over to the dithering engine and it takes care of the rest.
The engine is designed around monochrome conversion, but it could easily be modified to work on color palettes as well. The biggest difference with a color palette is that you must track separate errors for red, green, and blue, rather than a single luminance error. Otherwise, all the math is identical.
PhotoDemon v5.2 is now available. New features include selection tools, arbitrary rotation, HSL adjustments, CMYK support, new user preferences, multiple monitor support, and more. Download the update here.
New Feature: Selection Tool
Selections have been one of the top-requested PhotoDemon features since it first released, so I’m glad to finally be able to offer them. A lot of work went into making selections as user-friendly and powerful as possible.
Three render modes are provided. On-canvas resizing and moving are fully supported, as are adjustments by textbox (see screenshot above). Everything in the Color and Filter menus will operate on a selection if available, as well as the Edit -> Copy command.
(Note: as of this v5.2, selections are not yet tied into Undo/Redo, and selections will not be recorded as part of a Macro. These features will be added in the next release.)
New Feature: Crop to Selection
New Feature: HSL Adjustments
New Feature: Arbitrary (Free) Rotation
New Feature: CMY/K Rechanneling
New Feature: Sepia (W3C formula)
New Feature: Preferences Dialog (rewritten from scratch)
New preferences include:
Render drop shadows between images and canvas (similar to Paint.NET)
Full or compact file paths for image windows and Recent File shortcuts
Improved font rendering on Vista, Windows 7, and Windows 8 (via Segoe UI)
Remember the main window’s location between sessions
Loading and Saving:
Tone map imported HDR and RAW images
Options for importing all frames or pages of multi-image files (animated GIFs, multipage TIFFs)
Automatically clear selections after “Crop to Selection” is used
Pick your own transparency checkerboard colors
Pick from three transparency checkerboard sizes (4×4, 8×8, 16×16)
Allow PhotoDemon to automatically remove empty alpha channels from imported images
All preferences from v5.0 remain present, and there is now an option to reset all preferences to their default state – so experiment away!
New Feature: Recent File Previews (Vista, Windows 7, Windows 8 only)
New Feature: Multi-Image File Support (animated GIFs, multipage TIFFs)
New Feature: Waaaay better transparency handling, including adding/removing alpha channels
It’s hard to overstate how much better transparency support is in v5.2 compared to v5.0. Images with alpha-channels are now rendered as alpha in all viewport, filter, and tool screens. When printing, saving as 24bpp, or copying to the clipboard, transparent images are automatically composited against a white background. As mentioned previously, user preferences have been added for transparency checkerboard color and sizes.
PhotoDemon also allows you to add or remove alpha channels entirely. Here’s an example of an image with an alpha channel, and the associated “Image Mode” setting:
And here it is again, after clicking the “Mode -> Photo (RGB | 24bpp | no transparency)” option:
Finally, PhotoDemon now validates all incoming alpha channels. If an image has a blank or irrelevant alpha channel, PhotoDemon will automatically remove it for you. This frees up RAM, improves performance, and leads to a much smaller file size upon saving. (Note: this feature can be disabled from the Edit -> Preferences menu if you want to maintain blank alpha channels for some reason.)
New Feature: Custom “Confirm Unsaved Image(s)” Prompt
Improved Feature: Edge Detection
New Feature: Thermograph Filter
This Wikipedia article describes thermography in great detail. PhotoDemon’s thermography filter works by correlating luminance with heat, and analyzing the image accordingly. Here’s a sample, using a picture of the lovely Alison Brie, of Mad Men and Community fame:
New Feature: JPEG 2000 (JP2/J2K), Industrial Light and Magic (EXR), High-Dynamic Range (HDR) and Digital Fax (G3) image support
PhotoDemon now supports importing the four image types mentioned above, and it also supports JPEG 2000 exporting.
Multiple monitor support during screen captures (File -> Import -> Screen Capture)
Many miscellaneous interface improvements, including generally larger command buttons, text boxes, labels, and more uniform form layouts.
Many new and improved menu icons.
Heavily optimized viewport rendering. PhotoDemon now uses a triple-buffer rendering pipeline to speed up actions like zooming, scrolling, and using on-canvas tools like the new Selection Tool. Even when working with 32bpp images, all actions render in real-time.
Bilinear interpolation is now used during Isometric Conversion. This results in a much higher-quality transform. Hard edges are still left along the image border to make mask generation easy for game designers.
Vastly improved image previewing when importing from VB binary files.
Better text validation throughout the software. Invalid values are now handled much more elegantly.
More accelerator hotkey support, including changes to match Windows standards (such as Ctrl+Y for Redo, instead of the previous Ctrl+Alt+Z).
Update checks are now performed every ten days (instead of every time the program is run).
All extra program data – including plugins, preferences, saved filters and macros – have been moved to a single /Data subfolder. If you run PhotoDemon on your desktop, this should make things much cleaner for you.
PhotoDemon’s current and max memory usage is now displayed in the Preferences -> Advanced panel.
PhotoDemon 5.2 is nearing completion, and I need help testing it. Version 5.2 provides a bunch of new features, including selections, cropping, HSL adjustment, CMY/CMYK rechanneling, a new Sepia filter (based off the W3C standard), an overhauled preferences engine and interface, and more. Please download the beta and help me make sure everything is working properly.
Remember – if you are an advanced user, you can always download the most recent development build of PhotoDemon’s source code from its GitHub page.
PhotoDemon is funded by donations from users like you. Please consider a small donation to fund development and to help me support my family. Even $1.00 helps. Thank you!
List of what’s new and improved in v5.2 (so far)
Selection tool! It’s a hell of a tool, and a lot of work went into making it as user-friendly and powerful as possible. Three render modes are provided. On-canvas resizing and moving are fully supported as well. Everything in the Color and Filter menus will operate on a selection if available, as well as the Edit -> Copy command. (Note: as of this beta, selections are not yet tied into Undo/Redo, and selections will not be recorded as part of a Macro.)
Image cropping is now possible via the Crop-to-Selection option (in the Image menu).
New HSL adjustment tool. (See above screenshot for sample.)
New Rechannel tool. Live previews, CMY, and CMYK color spaces were added.
New Sepia filter based off the official W3C formula (available here). I still prefer PhotoDemon’s “Filters -> Antique” effect, but felt it was worthwhile to make both available.
Vast improvements to PhotoDemon’s support for images with transparency. Images with alpha-channels will now be rendered as alpha in all filter and tool screens. When printing, saving as 24bpp, or copying to the clipboard, the image will be composited against a white background. User preferences were also added for transparency checkerboard color and sizes.
All-new User Preferences dialog. Many new options were added, and the Preferences interface is now sorted by category.
Improved font rendering is now available for users on Windows Vista, Windows 7, and Windows 8.
A drop-shadow can now be rendered between the image and the canvas (similar to Paint.NET).
PhotoDemon is now capable of remembering its window size and position between sessions.
Multiple monitors are now supported by the Import -> Screen Capture tool.
Many miscellaneous interface improvements. Additionally, I am testing a new layout in the Color -> Grayscale tool. This layout style is intended to help users make sense of PhotoDemon’s many options. Let me know what you think, because if this style is popular, I will redo the other tool dialogs to match.
Heavily optimized viewport rendering. PhotoDemon now uses a triple-buffer rendering pipeline to speed up actions like zooming, scrolling, and using on-canvas tools like the new Selection Tool. Even when working with 32bpp images, all actions should render in real-time on any modern system.
Bilinear interpolation is now used in “Convert to Isometric Image”. This results in a much higher-quality transform. Hard edges are still left along the image border to make mask generation easy for game designers.
Last night, two hikers located my grandfather’s body. It was found at the bottom of a steep incline some 1.5 miles up Neff’s Canyon. His dog, Odin, was also found with him. The official report from the Medical Examiner will take some time to process, but it looks like the initial fall probably claimed both lives.
This news is difficult, but my family is so grateful to have some closure. Thank you again to all the heroes who helped us resolve this difficult situation – especially the detectives, police officers, firefighters, and community volunteers. A “Missing” poster of my grandfather was placed at the head of the trail where his body was found, and without that who knows if he would have been found before the winter snow hits.
Fritz was a kind, peaceful, brave and brilliant man. He will be dearly missed.
On Wednesday, 24 October 2012, my aunt stopped by Grandpa Helland’s home for a weekly visit. Grandpa wasn’t home, so my aunt called to make sure he was okay. He answered the phone call and said that he was on his way home, and he gave his location as Skyline High School which is only two blocks from his home. He was on a walk with his dog Odin, a miniature Doberman Pinscher, and there was no indication that anything was wrong.
By Sunday night, 17 square miles of Salt Lake City had been searched, including everything from I-80 to 5200 south, and from 1300 East to the benches, including Millcreek Canyon. Nearly two thousand volunteers participated in the official police-led search groups, while hundreds more searched informally.
On Monday, volunteer search efforts were called off. Due to the bitterly cold temperatures from Wednesday to Friday, and the length of time that grandpa has been missing, the efforts are now focused on finding his body. Cadaver dogs have been dispatched to high probability areas.
To summarize, here is what we know:
Fritz Helland is 80 years old. He is 5’10” and of thin build, maybe 165 lbs. He has white hair, blue eyes, and a distinctive Norwegian accent. He was last seen wearing a tan jacket.
Fritz was last seen with his dog, Odin, a miniature Doberman Pinscher. Odin has a black coat, with brown markings on his snout, chest, the inside of his ears, and his legs. Odin is also missing.
Two very likely sightings of Fritz and his dog occurred on Wednesday around 11:00a.m. These sightings occurred not far from his home (the location was 2700 east and 3500 south, specifically). Fritz appeared to be making his way east, toward home.
At 3:00 on Wednesday afternoon, Fritz’s daughter stopped by his home with dinner for the night. Fritz wasn’t there. His daughter called his cell phone, and Fritz answered. He told her he was walking past Skyline High School (which is roughly two blocks southeast of his home). There was no sign of anything being wrong. His daughter had to leave, but she left dinner for him on the kitchen table. This dinner was never touched, which along with other factors, makes it unlikely that Fritz ever made it home on Wednesday.
Fritz’s wallet, car keys, and vehicle were found at home. As far as we know, the only belongings he had with him at the time of his disappearance were his cell phone and the clothes on his back. (The dog was also with him.)
Cell phone records show that a tower near the high school handled the aforementioned phone call. This is not conclusive evidence of Fritz’s location, as he could be within several miles of that tower – but it does mean that at the time of the phone call, he was no more than five miles from home.
All subsequent phone calls have gone straight to voicemail, and the phone’s battery has since died. No better fix could be made on the location.
Regarding Fritz’s state of mind, he does have some issues with short-term memory – as would be expected of a typical 80-year-old male – but he is coherent, and to our knowledge he has never had an episode that would explain a disappearance like this. Additionally, his dog has been with him for 16 years and they have walked in the area every day during that time. It is unlikely that both he and the dog would not be able to find their way home.
Due to the snowstorm and bitterly cold temperatures on the evening of Fritz’s disappearance, a possible scenario is that he and the dog may have become disoriented and sought shelter – perhaps in a shed, a trailer, an abandoned home, or somewhere else out of the public eye. At this time, police are asking residents of Salt Lake City to search their sheds, trailers, and yards for any sign of Fritz or his dog. Cadaver dogs have also been dispatched to high priority areas to assist in this search.
If you know anything that may help us locate Fritz, please contact the Unified Police Department at 801-743-7000.
A number of sites have been assembled to help coordinate search efforts. These include:
If you have any relevant information to share, please share it with the police at 801-743-7000. Please do not send tips to me or other family members, as we will only direct you to the official police contact.
If you do not live in the Salt Lake City area, please still keep an eye out. The forceable abduction of an adult male and his dog would be extremely rare, but at this point we are not ruling anything out. Again, please report any possible sightings to the police at 801-743-7000.
Anything you can do to help spread the word is also appreciated. Facebook, Twitter, blogs, email – the venue doesn’t matter. The more people looking for grandpa, especially in the Salt Lake area, the better our chances of finding him.
My family and I are so incredibly grateful for the police and fire department’s efforts, especially that of Sheriff Winder and Detective Faulkner, as well as the overwhelming help from community volunteers. Thank you from the bottom of our hearts.
(I will do my best to update this page if more information becomes available.)