Code Monkey home page Code Monkey logo

Comments (34)

koszeggy avatar koszeggy commented on June 25, 2024 1

I cloned the repo but the SaveGifAnimation is different (uses BumpKit only). I tried to copy-paste the version you posted here but it contains unresolvable members. If you push your recent changes (maybe on a different branch if you don't want to break master) I can have a look at it later.

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024 1

I reopened the issue and improved LZW compression by using a custom hash table as I mentioned above.

The performance tests look promising: depending on how detailed the image is, the improvement is between 1.5x-5x

==[8 bpp Cleared Bitmap Results]================================================
Test Time: 2,000 ms
Warming up: Yes
Test cases: 2
Calling GC.Collect: Yes
Forced CPU Affinity: No
Cases are sorted by fulfilled iterations (the most first)
--------------------------------------------------
1. New: 7,434 iterations in 2,000.06 ms. Adjusted for 2,000 ms: 7,433.76
2. Original: 1,526 iterations in 2,000.75 ms. Adjusted for 2,000 ms: 1,525.43 (-5,908.33 / 20.52%)

==[8 bpp Alpha Gradient Non-dithered Results]================================================
Test Time: 2,000 ms
Warming up: Yes
Test cases: 2
Calling GC.Collect: Yes
Forced CPU Affinity: No
Cases are sorted by fulfilled iterations (the most first)
--------------------------------------------------
1. New: 2,009 iterations in 2,000.63 ms. Adjusted for 2,000 ms: 2,008.37
2. Original: 1,171 iterations in 2,000.31 ms. Adjusted for 2,000 ms: 1,170.82 (-837.55 / 58.30%)

==[8 bpp Alpha Gradient Dithered Results]================================================
Test Time: 2,000 ms
Warming up: Yes
Test cases: 2
Calling GC.Collect: Yes
Forced CPU Affinity: No
Cases are sorted by fulfilled iterations (the most first)
--------------------------------------------------
1. New: 1,080 iterations in 2,001.98 ms. Adjusted for 2,000 ms: 1,078.93
2. Original: 711 iterations in 2,002.53 ms. Adjusted for 2,000 ms: 710.10 (-368.83 / 65.82%)

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

it seems it is being quantized and not dithered even tho the original preview frames are already processed

What is the pixel format of the preview images? Are they indexed images with some palette (eg. 8 bpp)? If so, then the encoder will preserve their original content unless you set the Quantizer/Ditherer properties explicitly.

The provided code sample doesn't contain how previewFrames were generated. But please note that if you used the BitmapExtensions.Dither method, then the original pixel format is not changed (it can be still 24/32bpp, for example), it's just that the number of colors have been reduced. In this case the encoder quantizes the frames again by the Wu
quantizer (if you didn't set the Quantizer property), which adjusts the colors to the RGB555 color space, which may cause a small loss of details even if the image has only 256 colors. To create indexed preview images use the ImageExtensions.ConvertPixelFormat method instead.

If I use the default windows encoder saving is two times faster so there is something happening under the hood here...

If the input images are not indexed ones, then the default quantizer tries to optimize the palette for each frames individually, which has a significant impact on the performance. And considering that the default value of AnimatedGifConfiguration.AllowDeltaFrames is true, all consecutive frames are tried to be masked out based on the unchanged pixels compared to the previous frame, which also has some cost.

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

Thanks for the fast comment!
The images are just a list of Image that I processed to indexed format:

        public static Image ApplyEffects(Image image, Giffit.GiffitPreset preset)
        {
            if (preset.Scaling < 1)
                image = (Bitmap)ScaleImage(image, (int)(image.Width * preset.Scaling), (int)(image.Height * preset.Scaling));

            if (preset.StyleIndex != preset.DefaultStyle)
                image = image.ConvertPixelFormat(preset.pixFormat, preset.quantizer, preset.ditherer);

            return image;
        }

Then I want to save time by reusing the preview images and just write them to disc without conversions. Even thought I specified ditherer above it is not applied to the final image with the code I showed you in the first post so I have to assign it to the agf instance. Here is how settings look:

 public class GiffitPreset
    {
        public decimal Scaling = 1.0m;
        public int _sindex = 0;
        public IQuantizer quantizer = null;
        public IDitherer ditherer = null;
        public PixelFormat pixFormat = PixelFormat.Format8bppIndexed;

        public static List<string> StyleNames = new List<string>{
        "Graphix (1bpp)",
        "Lightness (1bpp)",
        "Polka Dot (1bpp)",
        "Medium Dot (1bpp)",
        "Small Dot (1bpp)",
        "Vintage (1bpp)",
        "Etchy (4bpp)",
        "Midnight Blues (4bpp)",
        "Mono Rough (4bpp)",
        "Grayscale (8bpp)",
        "Film Grain 200 (8bpp)",
        "Expired Film (8bpp)",
        "Full Index (8bpp)",
        "Colour #565 (16bpp)",
        "Transparante (16bppA)",
        "Colour #888 (24bpp)",
        "No Mercy (32bppA)",
        "Default System (fast)(8bpp)"
        };

        public int DefaultStyle { get => StyleNames.Count - 1; }
        public int StyleIndex
        {
            get { return _sindex; }
            set
            {
                _sindex = value;

                switch (value)
                {
                    case 0: // graphix
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite();
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise;
                        break;
                    case 1: // True mono
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite();
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = ErrorDiffusionDitherer.Atkinson;
                        break;
                    case 2: // Polka dot
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite();
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = OrderedDitherer.DottedHalftone;
                        break;
                    case 3: // medium dot
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite();
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = ErrorDiffusionDitherer.StevensonArce;
                        break;
                    case 4: // small dot
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite();
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = ErrorDiffusionDitherer.Burkes;
                        break;
                    case 5: // vintage
                        quantizer = OptimizedPaletteQuantizer.Octree(2);
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise.ConfigureStrength(.66f);
                        break;
                    case 6: // etchy
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite();
                        pixFormat = PixelFormat.Format4bppIndexed;
                        ditherer = ErrorDiffusionDitherer.StevensonArce;
                        break;
                    case 7: // blues
                        Palette blues = new Palette(new Color32[] {
                        new Color32(System.Drawing.Color.AliceBlue),
                        new Color32(System.Drawing.Color.SteelBlue),
                        new Color32(System.Drawing.Color.LightGoldenrodYellow),
                        new Color32(System.Drawing.Color.White),
                        new Color32(System.Drawing.Color.Black)});
                        quantizer = PredefinedColorsQuantizer.FromCustomPalette(blues);
                        pixFormat = PixelFormat.Format4bppIndexed;
                        ditherer = ErrorDiffusionDitherer.Burkes;
                        break;
                    case 8: // mono
                        quantizer = PredefinedColorsQuantizer.Grayscale16();
                        pixFormat = PixelFormat.Format4bppIndexed;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 9: // Gray
                        quantizer = PredefinedColorsQuantizer.Grayscale();
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 10: // film 200
                        quantizer = PredefinedColorsQuantizer.Grayscale();
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = new RandomNoiseDitherer(0.25f);
                        break;
                    case 11: // expired
                        quantizer = OptimizedPaletteQuantizer.Octree(36);
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise;
                        break;
                    case 12: // index
                        quantizer = OptimizedPaletteQuantizer.Octree();
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise;
                        break;
                    case 13: // 565 
                        quantizer = PredefinedColorsQuantizer.Rgb565();
                        pixFormat = PixelFormat.Format16bppRgb565;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 14: // transparante
                        quantizer = PredefinedColorsQuantizer.Argb1555();
                        pixFormat = PixelFormat.Format16bppArgb1555;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 15: // 888
                        quantizer = PredefinedColorsQuantizer.Rgb888();
                        pixFormat = PixelFormat.Format24bppRgb;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 16: // ultra
                        quantizer = PredefinedColorsQuantizer.Argb8888();
                        pixFormat = PixelFormat.Format32bppPArgb;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 17: // system default
                        break;
                    default:
                        quantizer = PredefinedColorsQuantizer.Rgb565();
                        pixFormat = PixelFormat.Format16bppRgb565;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                }
            }
        }

The image is confirmed to be 8bit after image array was created (Btw do you think I should add more info about your library in the about dialog? ):
image

Also does the delta frames improve file size or just looks?

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

Please note that >8 bpp quantizers will not be alright for GIF images (case 13..16). From the docs of the Quantizer property:

Should use up to 256 colors; otherwise, the result might be quantized further with using the default system 8-bit palette.

This image illustrates what happens in such case (captured from this tool) - note the tooltip that warns you about the undesired result:

image

It happens because a 32-bit quantizer was selected (which effectively just removes transparency below alphaThreshold as illustrated here). Using a ditherer with a 32 bit quantizer is also practically useless: there is no quantization error so its only effect is that partially transparent pixels are blended with back color (see also the images and their descriptions in the previous link).

Now, as the target pixel format is 8bpp, the result is even worse (as the screenshot illustrates). It's because the 32-bit quantizer returns a null Palette so the result will have the system default 8-bit palette. The quantization and dithering was processed in the 32-bit color space, not for the default palette, so the result looks like as if you didn't use any quantizer and ditherer while converting the pixel format to 8 bit.

How to create high-color GIF animations then (if this was your intention with high-color quantizers):

GIF format does not allow more than 256 colors per frame. High color animations use a clever trick: new frames do not clear the previous content and use a local palette to add up to 256 new colors compared to the previous frame. By reserving one palette entry for transparency it is possible to mask out the unchanged content. The AnimatedGifConfiguration makes it possible for you:

  • Pass the original, non-quantized images to the constructor
  • Set the Quantizer property to an OptimizedPaletteQuantizer (or to null to use the default Wu quantizer), so each frame can have their own optimized palette.
  • Set the AllowDeltaFrames property to true (actually this is the default value) so the encoder tries to exclude the unchanged pixels
  • Optionally, set the Ditherer property to improve the result quality.

Of course, when delta images are allowed, your preview for the non-first frame can only be a hint. The first frame will have 256 colors in all cases, and the further ones may have more. Actually, in the next release I plan to do some improvements when generating the delta images.

When can you pass the prequantized and dithered preview images to the encoder:

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

@koszeggy Thank you for the detailed explanation. I updated my styles to match your recommendations above:

 set
            {
                _sindex = value;

                switch (value)
                {
                    case 0: // graphix
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite();
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer =  OrderedDitherer.BlueNoise.ConfigureStrength(0.9f);
                        break;
                    case 1: // lightness
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite(System.Drawing.Color.Black, 92);
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = ErrorDiffusionDitherer.Atkinson.ConfigureErrorDiffusionMode(true);
                        break;
                    case 2: // Polka dot
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite(System.Drawing.Color.Black, 112);
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = OrderedDitherer.DottedHalftone.ConfigureStrength(.9f);
                        break;
                    case 3: // medium dot
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite();
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = ErrorDiffusionDitherer.StevensonArce;
                        break;
                    case 4: // small dot
                        quantizer = PredefinedColorsQuantizer.BlackAndWhite(System.Drawing.Color.Black, 24);
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = ErrorDiffusionDitherer.Burkes;
                        break;
                    case 5: // vintage
                        quantizer = OptimizedPaletteQuantizer.Octree(2);
                        pixFormat = PixelFormat.Format1bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise.ConfigureStrength(.66f);
                        OptimisedQuantizer = true;
                        break;
                    case 6: // mono pop
                        quantizer = PredefinedColorsQuantizer.Grayscale4(System.Drawing.Color.Black, true);
                        pixFormat = PixelFormat.Format4bppIndexed;
                        ditherer = OrderedDitherer.Bayer8x8.ConfigureStrength(.7f);
                        break;
                    case 7: // blues
                        Palette blues = new Palette(new Color32[] {
                        new Color32(System.Drawing.Color.AliceBlue),
                        new Color32(System.Drawing.Color.SteelBlue),
                        new Color32(System.Drawing.Color.LightGoldenrodYellow),
                        new Color32(System.Drawing.Color.White),
                        new Color32(System.Drawing.Color.Black)});
                        quantizer = PredefinedColorsQuantizer.FromCustomPalette(blues);
                        pixFormat = PixelFormat.Format4bppIndexed;
                        ditherer = ErrorDiffusionDitherer.Burkes;
                        break;
                    case 8: // mono
                        quantizer = PredefinedColorsQuantizer.Grayscale16();
                        pixFormat = PixelFormat.Format4bppIndexed;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 9: // Gray
                        quantizer = PredefinedColorsQuantizer.Grayscale();
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 10: // film 200
                        quantizer = PredefinedColorsQuantizer.Grayscale();
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = new RandomNoiseDitherer(0.25f);
                        break;
                    case 11: // filmic
                        quantizer = OptimizedPaletteQuantizer.Octree(32);
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise.ConfigureStrength(.88f);
                        OptimisedQuantizer = true;
                        break;
                    case 12: // index
                        quantizer = OptimizedPaletteQuantizer.Octree();
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise.ConfigureStrength(.9f);
                        OptimisedQuantizer = true;
                        break;
                    case 13: // 332
                        quantizer = PredefinedColorsQuantizer.Rgb332(System.Drawing.Color.Black, true);
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 14: // High fidelity
                        HighQuality = true;
                        quantizer = OptimizedPaletteQuantizer.Wu();
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                    case 15: // system default
                        break;
                    default:
                        quantizer = PredefinedColorsQuantizer.Rgb332();
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = ErrorDiffusionDitherer.FloydSteinberg;
                        break;
                }

Btw the speed did not improve compared to this native encoder part of BumpKit. And I think I have figured out something. For most of the cases the ditherer is passed trough from the settings and generated images I provide but if I use setting 9 Grayscale the save becomes faster and in the end there is no ditherer for some reason except on the first frame? So I believe that there might be a case where the ditherer is reapplied and that's why the saving time gets much longer for the already generated preview images. Here's an example gif:

animation2

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

As for the speed:

  • Using ErrorDiffusionDitherer or the RandomNoiseDitherer with a non-null seed disables parallel processing so if you have many cores, they are much slower than other ditherers
  • OptimizedPaletteQuantizer is slow. Even the fastest Wu is slower than any PredefinedPaletteQuantizer
  • Some PredefinedPaletteQuantizer members are faster than the others. BlackAndWhite and Grayscale are really fast because they don't use any palette lookup. Instead, they map any color to a palette entry directly by brightness. Similarly, Grayscale4, Grayscale16 and Rgb332 can also be faster if the directMapping parameter is true, though the result may have a higher contrast than by the default nearest color lookup behavior.

As for dithering:

  • You didn't provide the corresponding code (the attached file contains binaries only) so I can only guess why only the first frame is dithered. Are you sure you pass the original, non-quantized images to the encoder? Please note that if a frame is already quantized, then the ditherer has no chance to restore the original details, and it will do nothing because there will no quantization error when quantizing the the already processed frames again.
  • The Grayscale quantizer can map any color to one of the 50 256 shades of gray without quantization error, therefore it makes no sense to use a ditherer in this case. Error diffusion will have no effect, and for other ditherers you should explicitly set strength to force the dithering patterns to appear. But it definitely affects all frames, not just the first one:
    Png2Gif_Balls_Gr_Rnd (grayscale quantizer and random noise ditherer with some forced strength)

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024
  • The Grayscale quantizer can map any color to one of the 50 256 shades of gray without quantization error, therefore it makes no sense to use a ditherer in this case. Error diffusion will have no effect, and for other ditherers you should explicitly set strength to force the dithering patterns to appear. But it definitely affects all frames, not just the first one:

I think If I specify none, then I get this result with the banding. Maybe there is something somewhere happening that should not be. Maybe I can add you to my repository but all the code wont help more I think. Here is the save code:

 public static Image ApplyEffects(Image image, Giffit.GiffitPreset preset)
        {
            if (preset.Scaling < 1)
                image = (Bitmap)ScaleImage(image, (int)(image.Width * preset.Scaling), (int)(image.Height * preset.Scaling));

            if (preset.StyleIndex != preset.DefaultStyle)
                if (!preset.HighQuality)
                    image = image.ConvertPixelFormat(preset.pixFormat, preset.quantizer, preset.ditherer);

            return image;
        }

 protected void SaveGifAnimation()
        {
            double time = 1000 / (double)nudFPS.Value;
            double percentMultiplier = 100 / previewImages.Count;
            IEnumerable<IReadableBitmapData> imageArray;

            if (settings.HighQuality)
            {
                List<Image> allframes = new List<Image>();
                allframes.AddRange(originalImages);
                allframes.AddRange(originalImagesLoopBack);
                // just apply the resize here
                allframes = allframes.Select(i => ApplyEffects(i, settings)).ToList();
                imageArray = allframes.Select(i => BitmapExtensions.GetReadableBitmapData((Bitmap)i.Clone()));
            }
            else
                imageArray = previewImages.Select(i => BitmapExtensions.GetReadableBitmapData((Bitmap)i.Clone()));

            var agf = new AnimatedGifConfiguration(imageArray, TimeSpan.FromMilliseconds(time));
            agf.AnimationMode = (AnimationMode) nudRepeat.Value;
            agf.SizeHandling = AnimationFramesSizeHandling.Center;
            agf.ReportOverallProgress = true;

            if (settings.OptimisedQuantizer)
                agf.AllowDeltaFrames = false;

            // bugfix for now
            if (settings.StyleIndex == 9) agf.Ditherer = settings.ditherer;

            if (settings.HighQuality)
            {
                // must be agf.AllowDeltaFrames = true;
                agf.Quantizer = settings.quantizer;
                agf.Ditherer = settings.ditherer;
            }

            using (var stream = new MemoryStream())
            {
                var progresseReporter = new TaskConfig();
                progresseReporter.Progress = new SaveProgress(saveGIF.FileName, UpdateInfo);
                
                GifEncoder.EncodeAnimationAsync(agf, stream, progresseReporter).Wait();

                stream.Position = 0;
                using (var fileStream = new FileStream(saveGIF.FileName, FileMode.Create, FileAccess.Write, FileShare.None, 131072, false))
                {
                    stream.WriteTo(fileStream);
                }
            }
        }

For some reason I cant push the latest changes but I think what you can see is not so far behind.
image

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

I will try to figure it out but there is nothing in the Output window :D

@koszeggy I think its ok now and you can check the latest commit

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

I could reproduce the banding issue. It's because AnimatedGifConfiguration.Quantizer is null. In this case the default Wu quantizer should not be used for already indexed images, still, it is applied for some reason. And the banding appears because the Wu quantizer selects colors from the RGB555 color space so it uses only 32 shades.

Possible workarounds:

  • Set the AnimatedGifConfiguration.Quantizer property to PredefinedColorsQuantizer.Grayscale even if the source frames are already quantized.
  • Or, if you already have indexed images, you can instantiate the GifEncoder class and add the frames manually, instead of using the static EncodeAnimation method. Do not forget to set the global palette if you used some PredefinedColorQuantizer so it will not be stored for all frames.

Further notes:

  • Setting FloydSteinberg ditherer for the Grayscale quantizer will have no effect, but it has an impact on the performance (as error diffusion ditherers disable parallel processing)
  • Try to avoid using a specific strength for ditherers, (except for artistic effects, such as random white noise). Btw, auto calibration uses strength=1 for black and white palette, so 0.9 will be far too strong for 256 colors, especially for Octree quantizer (case 12).
  • I recommend to make back color and alpha threshold configurable. See the following comparisons:

animation_Grayscale - case 9: black background with banding (I didn't even notice it firstly but it is visible if zoomed)
PngToGif Silver - silver background, quantizer is set in encoder config so there is no banding
animation_BumpKit - case 15 'System default' (by BumpKit)
animation_high - case 14 'High fidelity' result (Wu quantizer with default black background, 128 alpha threshold)
animation_high_white_16 - Wu quantizer with white background, alpha threshold is 16

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

Sorry for late reply but I was trying to implement at least the colour for this release.

I'm an architect and just want the fastest possible solution to prepare graphic material. I think the threshold should wait for next releases as the software I use generates the images and the animations without transparency better. I would definitely add the option in the future but for the moment, given the regular input files, I thinks its not that necessary. More important is performance with high res files as regularly I render at more than 2000px. There are cases of course where I export images for web and then only the artistic qualities are judged rather than absolute accuracy. I think I'm after smooth gradients and also distinct shadows. I checked your code but could not figure out why there is delay as it is doing similar thing to the BumpKit. It should be super fast as it already has the formatted images and just have to write them to disk. Is there some compression happening like LZW on the way? But actually overall performance now is comparable to Photoshop gif editor with similar resolution images. Here is an artistic result:

animation

I also attach an archive with sample files (very few because there is size limit on github) I would use without scaling down. The transparent option does not render properly as in software for some reason.
2021.12.24.zip
alpha rotate.zip
parallel.zip

I checked what you suggested but in the end I set again 8x8 ditherer because otherwise I get the ditherer from the previous style I was using for some reason. You can check the latest commit.

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

Is there some compression happening like LZW on the way?

Yes. Actually I tried to optimize it as much as possible, for example, instead of the default Span<byte> hash code I implemented a custom one so the current LZW compression is now about 8-10x faster than the initial implementation. But I still have a lower-prio plan to to improve it further by using a completely custom hashing storage instead of the Dictionary<TKey, TValue> class.

To improve performance with the current version you can try to instantiate the GifEncoder class so it allows you to set the CompressionMode. For 8bpp images the DoNotIncreaseBitSize can be a good balance between speed and produced file size. For B&W images this option produces almost as large result than with the uncompressed option though.

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

I will try to do that next year and will start working on it soon. Thanks so much for the help you are giving me. Actually I have a question? Why did you decide to make this library open source? I wonder how to license my program. I have no problem listing it as open source but fist I want to know what are my options regarding future growth and support. Does it mean that other people get to make money out of the free software you write?

And finally almost forgot. Happy New Year!

Edit:
Actually I did some tests on 332 and it is as fast as the system encoder but with much better quality for somethin like 0.5% increase in file size. The high fidelity is quite big in file size as well as theOctree even in 36 colours. I think I will make 332 the default encoder style and I just saw you released 6.2 so have to update that as well.

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

The possible accidental re-quantizing issue has been fixed by this commit. But it's not in 6.2.0 so it will be released later.

Why did you decide to make this library open source?

Enthusiasm :)

Does it mean that other people get to make money out of the free software you write?

They are allowed to do so. I didn't set up any donation yet so I have no idea whether anyone would be grateful for it.

Actually I did some tests on 332 and it is as fast as the system encoder but with much better quality

The system palette is a bit redundant with its first 16 colors, and also has 24 transparent entries. RGB332 does not have a transparent color though.

The high fidelity is quite big in file size as well as theOctree even in 36 colours

It's because by using an optimized quantizer all frames will have their own palette, whereas the RGB332 palette is stored only once as a global one.

Happy 2022 to you, too!

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

The performance tests look promising: depending on how detailed the image is, the improvement is between 1.5x-5x

That sounds amazing! Looking forward to updating to that version when its ready. Actually it seems you already published the changes in 6.2.1.
Btw I think I misunderstand something. Allow delta frames should be true only when I create High fidelity gif or optimised quantizer is used right? Otherwise the filesize of the predefined palette gifs is double.

 var agf = new AnimatedGifConfiguration(imageArray, TimeSpan.FromMilliseconds(time));
            agf.AnimationMode = (AnimationMode)nudRepeat.Value;
            agf.SizeHandling = AnimationFramesSizeHandling.Center;
            agf.ReportOverallProgress = true;
            agf.ReplaceZeroDelays = false;
            agf.AllowDeltaFrames = false;

            if (settings.OptimisedQuantizer)
                agf.AllowDeltaFrames = true;

            // grayscale bugfix for now
            if (settings.StyleIndex == 9) agf.Quantizer = settings.quantizer;

            if (settings.HighQuality)
            {
                // must be agf.AllowDeltaFrames = true;
                agf.Quantizer = settings.quantizer;
                agf.Ditherer = settings.ditherer;
            }

I think we should set up some form of donation just in case. I have no idea if someone would be interested in my software as well but who knows.

PS:
When I check 'enable ready to run' option and I start the program I get an error. If that option is not enabled it runs fine even as a packaged exe. Do you have any idea why is that and if this option makes any sense to use at all:
image

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

Actually it seems you already published the changes in 6.2.1.

It's not released yet. I'm just maintaining the change log. The actual version can be even 6.3.0 if there will be more changes.

Allow delta frames should be true only when I create High fidelity gif or optimised quantizer is used right?

Not necessarily. It can be true whenever the palette supports transparency, even for system default palette. It's just that with a fix palette there will never be more than 256 colors on a frame, but masking out the unchanged parts may help to reduce the file size.

When I check 'enable ready to run' option and I start the program I get an error.

Now that's interesting. It says it cannot find KGySoft.CoreLibraries 6.0.1. By any chance, do you have maybe an explicit reference to KGySoft.CoreLibraries with a specific version? Maybe an auto-generated assembly redirect in app.config, versioned reference in .csproj that didn't change when you upgraded the KGySoft.Drawing version, etc?

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

Not necessarily. It can be true whenever the palette supports transparency, even for system default palette. It's just that with a fix palette there will never be more than 256 colors on a frame, but masking out the unchanged parts may help to reduce the file size.

Got it.. fixed that.
I use the NuGet package so not sure how everything is linked together. I will wait for the next update and see how things go.

There is something more interesting happening though. When I use optimized palette images that support transparency and save already quantized frames the resulting file has lower resolution than frames I'm passing to the SaveAnimation method:

image

compared to result. It seems only the information about frame size is wrong but the actual saved images are the desired resolution because when I open the gif in my program it shows correct size for all frames:

image

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

I use the NuGet package so not sure how everything is linked together. I will wait for the next update and see how things go.

Feel free to build it by yourself, it's on the Development branch. It's not always stable and I'm just working on a larger refactoring right now.

resulting file has lower resolution than frames I'm passing to the SaveAnimation method [...] It seems only the information about frame size is wrong but the actual saved images are the desired resolution

The file Properties/Details tab is a bit misleading. GIF specification allows you to add smaller frames than the actual image size (or logical screen size as it is called in the specs). If you instantiate the GifEncoder by its constructor, then you can specify the logical screen size, and when you add a frame, you can add smaller images specifying their location within the logical screen. For some reason the Details tab shows you see the dimensions of the first frame rather than the actual image size.

And it happens because if the EncodeTransparentBorders property is false (this is the default value), then the encoder attempts to trim the transparent areas of each frame and encodes only the smallest possible rectangle with some actual content. From the linked documentation (note the emphasis):

If true, then transparent borders of the frames will be considered as image content (and possibly smaller frames will be virtually enlarged, too). This produces a bit larger encoded size but provides better compatibility.
If false, then always only the smallest possible non-transparent area will be encoded. Some decoders may not tolerate this option.

To "fix" the reported resolution in file details you can set the EncodeTransparentBorders property to true so the borders will also be encoded as actual image content.

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

To "fix" the reported resolution in file details you can set the EncodeTransparentBorders property to true so the borders will also be encoded as actual image content.

I knew I was doing something wrong :D Okey it makes sense to remove them but when preview them they seemed smaller scale than with the borders which should not be the case but I guess that's how the windows photo app works. Using GifEncoder is maybe not necessary at the moment but the EncodeAnimation instead.

I also tried to build it but it seems I'm lacking System.Drawing.Common v6 on NET 5 and don't know how to get it. So I'm grateful there is NuGet package as well.

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

Hello,

I updated to the latest versions and now the Octree is also doing the same things as the Grayscale from before:
image

I will check tomorrow if the settings are not wrong on my side but I did not change anything from before on the save part.

P.S.
I dont get the 'enable ready to run' exception anymore

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

The fix has not been released yet because in the end I will introduce more changes in the next release. That's why the issue is still open. Feel free to download the Development branch if you want to try it out.

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

The fix has not been released yet because in the end I will introduce more changes in the next release. That's why the issue is still open. Feel free to download the Development branch if you want to try it out.

Alright, I will try. I was sleepy yesterday and pulled the wrong branch. There is something that I don't understand thought. Why the final app size have increased double when I used the manually referenced assemblies vs the NuGet ones? It was 4MB and now is 8MB.

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

Why the final app size have increased double when I used the manually referenced assemblies vs the NuGet ones? It was 4MB and now is 8MB.

Just a guess: you mentioned you build a single-file monolith build. Maybe NeGet references are not directly included (is there a *.deps.json in the output dir?), but when directly referenced, all will be included in the final .exe file. However, I'm not sure about this as I haven't played too much with this build option yet.

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

Yeah maybe that is what is happening but as I'm not too experienced and 8MB is not too big its fine. I just use this option because I dont have an installer yet.

P.S.
I updated to the development version now of the Drawing.dll and it seems the performance improvement is really visible. The octree on colour images is really fast but on grayscale the same problem occurs as the 'Grayscale' preset with the banding and performance decreases as it seems it is reapplying things. The 'Grayscale' is fixed thought. Here is my latest presets for grayscale and Octree:

                    case 9: // Gray
                        quantizer = PredefinedColorsQuantizer.Grayscale(Background);
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = OrderedDitherer.Bayer8x8; // because preview does not work otherwise and is using the previous detherer
                        break;
                    case 10: // film 200
                        quantizer = PredefinedColorsQuantizer.Grayscale(Background);
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = new RandomNoiseDitherer(0.25f);
                        break;
                    case 11: // rought c
                        Color[] colour9 = new Color[] {
                            System.Drawing.Color.Black,
                            System.Drawing.Color.White,
                            System.Drawing.Color.Transparent,
                            System.Drawing.Color.Red,
                            System.Drawing.Color.Lime,
                            System.Drawing.Color.Blue,
                            System.Drawing.Color.Cyan,
                            System.Drawing.Color.Yellow,
                            System.Drawing.Color.Magenta
                        };
                        quantizer = PredefinedColorsQuantizer.FromCustomPalette(colour9, Background, AlphaThold);
                        pixFormat = PixelFormat.Format4bppIndexed;
                        ditherer = ErrorDiffusionDitherer.Stucki;
                        // not optimised but use this for setting delta frames to true as we have transparency
                        OptimisedQuantizer = true;
                        break;
                    case 12: // old colour 
                        quantizer = OptimizedPaletteQuantizer.MedianCut(16, Background, AlphaThold);
                        pixFormat = PixelFormat.Format4bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise;
                        OptimisedQuantizer = true;
                        break;
                    case 13: // filmic
                        quantizer = OptimizedPaletteQuantizer.Octree(32, Background, AlphaThold);
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise.ConfigureStrength(.88f);
                        OptimisedQuantizer = true;
                        break;
                    case 14: // full index
                        quantizer = OptimizedPaletteQuantizer.Octree(256, Background, AlphaThold);
                        pixFormat = PixelFormat.Format8bppIndexed;
                        ditherer = OrderedDitherer.BlueNoise;
                        OptimisedQuantizer = true;
                        break;
                  
                

EDIT:
Actually if I turn off AllowDeltaFrames it works fast and with good results. Should I turn off delta frames at all and just keep it for the high fidelity option?

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

If you push the recent changes I will have a look at it.

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

I updated it just now. Thanks

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

Are you sure you built the latest Development branch? You should have faced with different kind of errors, such as transparent background was not always cleared...

Btw, this kind of banding should not occur anymore even when requantizing with default Wu quantizer because after some recent changes it uses the RGB777 color space by default instead of RGB555 so monochrome input frames will use 127 shades instead of just 32.

I attached the .NET 5 release build of my current Development branch. Please let me know if it works for you. And please note that it cannot be considered as a stable version (I would like release the next version in a week or so): KGySoft.Drawing.zip

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

I think I did the development one as I clicked on it and then press open in VS?
image

Don't know what is happening.. But the dll you sent me is much faster for 8bpp images and I dont see the banding issue any more! The B&W styles seems slow to save now when they looked fast before.. massive speed increase. I will wait for the stable release before I update but there is no rush at all.

I realised something, when I publish there is now no debug symbols for the Drawing dll, so I must have built a debug one? But on the toolbar it says release?
image

Btw I was thinking that the green background I set on the 332 style is cool but seems there are some dots on it:
image

If I want to do a style where the background is a good solid colour what quantizer and ditherer I should use? I was thinking if I can get out the transparency colour from the Octree or Wu maybe that would work or make custom palette without transparency?

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

The screenshot seems to be good. Make sure sure you reference the actual built assemblies. For example, I noticed that you also have an older version of the .dll under source control.

I realised something, when I publish there is now no debug symbols for the Drawing dll, so I must have built a debug one?

I attached a release build without debug symbols.

Btw I was thinking that the green background I set on the 332 style is cool but seems there are some dots on it:

The image appears to be dithered. The color Green (0;128;0) cannot be found in the RGB332 palette (closest greens are 0;109;0 and 0;146;0) so the ditherer just did its job. You can pick any back color even for a B&W palette so the background will have some non-solid pattern if dithered.

Quantizing with RGB332 palette, using Color.Green background and Floyd-Steinberg dithering:
closest green by dithering

If I want to do a style where the background is a good solid colour what quantizer and ditherer I should use?

If you want a solid back color either use no ditherer (so simply the closest color will be used) or use a back color from the palette so there will be no quantization error for the ditherer to compensate. Of course, ditherers with strength can be forced to use a too strong dithering so the patterns may appear even with a color from the palette.

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

Sorry for late reply but had lots of things to do. How did you see that I have old assembly? Is that in github site or in VS?

Okey got the concept about the solid colour and will be making a style without dithering. In the future I can make style editor and there users can be able to select more settings. Thanks for explaining!

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

How did you see that I have old assembly? Is that in github site or in VS?

In your Tests folder. Though it might not be one you reference.

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

Ah that is just for the Imaging tools app. I reference directly from the release folders, which is not the smartest solution but for now its ok.

from kgysoft.drawing.

koszeggy avatar koszeggy commented on June 25, 2024

The fix has been released in v6.3.0

from kgysoft.drawing.

dilomo avatar dilomo commented on June 25, 2024

Great! Thank you :)

from kgysoft.drawing.

Related Issues (10)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.