Skip to content

Conversation

@br3aker
Copy link
Contributor

@br3aker br3aker commented Jul 17, 2021

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

fixes #1705
Color deduction is improved, tested on some images - results are promising!
Moved everything quantization table-wise to the Quantization.cs

Image at #845 had invalid test 'quality', it should have been rounded, not ceiled, set it from 99 to 98 for now.

TODO

  • encoder should select quantization tables based on new API
  • docs
  • tests
    nint usage
  • tests to calculate `is this variance standard enough?' in practice because we actually need to determine if table is standard

Copy link
Contributor Author

@br3aker br3aker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have some questions about internal API

@codecov
Copy link

codecov bot commented Jul 17, 2021

Codecov Report

Merging #1706 (1e6547b) into master (4638965) will decrease coverage by 0.04%.
The diff coverage is 97.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1706      +/-   ##
==========================================
- Coverage   84.41%   84.36%   -0.05%     
==========================================
  Files         822      822              
  Lines       36063    36022      -41     
  Branches     4207     4204       -3     
==========================================
- Hits        30443    30391      -52     
- Misses       4802     4816      +14     
+ Partials      818      815       -3     
Flag Coverage Δ
unittests 84.36% <97.00%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
src/ImageSharp/Formats/Jpeg/JpegDecoder.cs 95.23% <ø> (ø)
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs 89.47% <ø> (ø)
src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs 50.00% <50.00%> (-5.56%) ⬇️
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs 92.41% <84.61%> (+0.45%) ⬆️
...rc/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs 82.74% <100.00%> (-3.65%) ⬇️
...p/Formats/Jpeg/Components/Decoder/JpegComponent.cs 93.87% <100.00%> (ø)
...ImageSharp/Formats/Jpeg/Components/Quantization.cs 100.00% <100.00%> (ø)
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs 94.79% <100.00%> (-0.37%) ⬇️
src/ImageSharp/Formats/Jpeg/JpegMetadata.cs 100.00% <100.00%> (ø)
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4638965...1e6547b. Read the comment docs.

@br3aker
Copy link
Contributor Author

br3aker commented Jul 18, 2021

@JimBobSquarePants @antonfirsov

Did some research on estimator and it works very well actually predicting matrices produced by the encoder:

Luminance
Variances for Luminance table.
Quality levels 25-100:
q=25	0	est. q: 25
q=26	0,6900486702215858	est. q: 26,024412840373362
q=27	1,4706823623128003	est. q: 27,024602491459646
q=28	1,2785469780174026	est. q: 28,112191569091046
q=29	1,1775001233145304	est. q: 29,040546617422844
q=30	1,4174248602284933	est. q: 30,07976440516557
q=31	1,4646344796710764	est. q: 31,02076929396441
q=32	1,2118263731099432	est. q: 32,02205643673543
q=33	0,8141364518887713	est. q: 33,11632561401876
q=34	1,6665811662714987	est. q: 33,96628505199738
q=35	0,9232605200741091	est. q: 35,177795215278366
q=36	1,1127103053840983	est. q: 36,27815962938035
q=37	1,378856307335809	est. q: 37,002086926012055
q=38	1,0856774119383772	est. q: 38,22325158100902
q=39	1,1775001233163493	est. q: 39,115387591973736
q=40	1,4400911343473126	est. q: 39,86247084081212
q=41	1,3767202763010573	est. q: 41,29910411058831
q=42	1,234173531009219	est. q: 41,97338990890838
q=43	1,1282963146550173	est. q: 43,00023311570005
q=44	1,3585401036325493	est. q: 44,26381812775582
q=45	1,458902856309578	est. q: 45,04201720830167
q=46	0,6900486702379567	est. q: 46,350927451413654
q=47	1,088109967751734	est. q: 47,121053433942755
q=48	2,2080337819988927	est. q: 48,040862469704074
q=49	1,0528940597578185	est. q: 49,28736107087304
q=50	0	est. q: 50
q=51	1,0528931625449331	est. q: 50,722942454740405
q=52	2,2080339720541815	est. q: 52,03903312794864
q=53	1,0881065990797651	est. q: 53,05484114214778
q=54	0,6900482721739536	est. q: 53,93635290674865
q=55	1,220181547147149	est. q: 54,903984162956476
q=56	1,3714545976245063	est. q: 55,983932269737124
q=57	1,365342979261186	est. q: 57,04243411310017
q=58	1,1282981840340653	est. q: 58,139219880104065
q=59	1,1410138193514285	est. q: 59,037849912419915
q=60	1,1407521721157536	est. q: 59,940363466739655
q=61	1,2785477334891766	est. q: 61,07061146758497
q=62	1,2103239791313172	est. q: 61,95206558331847
q=63	1,2172928992104062	est. q: 63,06559508666396
q=64	1,1775002723243233	est. q: 63,91346617601812
q=65	1,0665629485201862	est. q: 65,00663976185024
q=66	1,3845724134389457	est. q: 65,94032463617623
q=67	1,4174274697306828	est. q: 66,8876466806978
q=68	1,1324866958821076	est. q: 68,01257678307593
q=69	1,1127106472777086	est. q: 68,91198516823351
q=70	1,4293262576607049	est. q: 70,06213702261448
q=71	0,9232593122746948	est. q: 71,06755869463086
q=72	1,2118242887463566	est. q: 71,92881139926612
q=73	1,7028581219874468	est. q: 72,91174442507327
q=74	1,2061910146539958	est. q: 74,16409873403609
q=75	0,8058072853714293	est. q: 74,74685530178249
q=76	1,2061902484078928	est. q: 75,83590135909617
q=77	1,7028572779545357	est. q: 77,08825510926545
q=78	1,2118248299298102	est. q: 78,0711883213371
q=79	0,923259961500662	est. q: 78,93244128208607
q=80	1,4293257435849682	est. q: 79,93786276783794
q=81	1,112709965312888	est. q: 81,08801490161568
q=82	1,1324864905966479	est. q: 81,98742298409343
q=83	1,4174264429316281	est. q: 83,11235310975462
q=84	1,3845713790170748	est. q: 84,05967517755926
q=85	1,049529159115309	est. q: 84,93440570309758
q=86	1,1775005830131704	est. q: 86,08653354458511
q=87	1,2172932673361174	est. q: 86,93440470378846
q=88	1,2103243191085085	est. q: 88,04793453309685
q=89	1,2785471962706083	est. q: 88,92938854405656
q=90	1,1407528967250187	est. q: 90,05963645176962
q=91	1,1410135141355227	est. q: 90,96215004101396
q=92	1,1282975903178567	est. q: 91,86078009661287
q=93	1,365343046062577	est. q: 92,95756578212604
q=94	1,3714546697582364	est. q: 94,0160675381776
q=95	1,2515629371297763	est. q: 95,03706148243509
q=96	0,6900487907659567	est. q: 96,06364700011909
q=97	1,0881073654912399	est. q: 96,94515896844678
q=98	2,3629059983706604	est. q: 97,68161100219004
q=99	5,001353712778048	est. q: 98,32455397117883
q=100	0	est. q: 100
Min variance: 0
Max variance: 5,001353712778048
Chrominance
Variances for Chrominance table.
Quality levels 25-100:
q=25	0	est. q: 25
q=26	0,3289928270824021	est. q: 26,03743994664574
q=27	0,2567842723437934	est. q: 27,064679348888934
q=28	0,19492864389758324	est. q: 28,125373916432874
q=29	0,15367341504315846	est. q: 29,10831780075463
q=30	0,12293467960626003	est. q: 30,16416949602972
q=31	0,24340592175576603	est. q: 31,106414456925577
q=32	0,4086034253741673	est. q: 32,12127202644116
q=33	0,23096369710401632	est. q: 33,20182788235059
q=34	0,40788787931160186	est. q: 33,960405383481834
q=35	0,1754530939288088	est. q: 35,09803580540892
q=36	0,19451782029500464	est. q: 36,156069992036116
q=37	0,3453769973530143	est. q: 37,003982164021295
q=38	0,456310679546732	est. q: 38,09049339964678
q=39	0,15367341504315846	est. q: 38,99310668528405
q=40	0,36991917603336333	est. q: 39,88931618100753
q=41	0,3158340733843943	est. q: 41,2617105814941
q=42	0,33136279402606306	est. q: 41,96017561784348
q=43	0,15366260785413033	est. q: 43,02776360067011
q=44	0,2676441431249259	est. q: 44,25175909324916
q=45	0,1294194163674547	est. q: 44,99780749316061
q=46	0,3289928270805831	est. q: 46,30966084423982
q=47	0,23879525277334324	est. q: 47,1641921199368
q=48	0,13732431547396118	est. q: 48,01189636312582
q=49	0,47396762017160654	est. q: 49,08130488693843
q=50	0	est. q: 50
q=51	0,4739676361987222	est. q: 50,93589103780687
q=52	0,13732498997160292	est. q: 52,07042586989701
q=53	0,23879479229617573	est. q: 53,00631681457162
q=54	0,3289942392184457	est. q: 53,984418185427785
q=55	0,2589643885685291	est. q: 55,09069343097508
q=56	0,13713482134426158	est. q: 56,068794848397374
q=57	0,4446088114455051	est. q: 57,091491390019655
q=58	0,1536636163955336	est. q: 58,10201824642718
q=59	0,2281612571041478	est. q: 59,041866986081004
q=60	0,3358503991194084	est. q: 60,069571482017636
q=61	0,1949285494411015	est. q: 61,11229262314737
q=62	0,21875850971719046	est. q: 62,05319166183472
q=63	0,3679954791741693	est. q: 63,08983927592635
q=64	0,15367232371136197	est. q: 64,11389419808984
q=65	0,28967497732719494	est. q: 65,07345945574343
q=66	0,2991529692144468	est. q: 66,13836633041501
q=67	0,12293578855769738	est. q: 67,12021087296307
q=68	0,4809884522892389	est. q: 68,12198660336435
q=69	0,19451708518090527	est. q: 69,14468077011406
q=70	0,36365722516802634	est. q: 70,13153568841517
q=71	0,175452534112992	est. q: 71,22905980795622
q=72	0,40860331005569606	est. q: 72,16995642520487
q=73	0,1974451000164663	est. q: 73,18900562822819
q=74	0,364259353626494	est. q: 74,24038457684219
q=75	0,1931513081635785	est. q: 74,74056840874255
q=76	0,3642587075200936	est. q: 75,75961533002555
q=77	0,1974445989171727	est. q: 76,81099416222423
q=78	0,4086036432572655	est. q: 77,83004227094352
q=79	0,17545300647134354	est. q: 78,77094012219459
q=80	0,3636563494492293	est. q: 79,86846421845257
q=81	0,19451726894635613	est. q: 80,8553179493174
q=82	0,4809880078362312	est. q: 81,87801335006952
q=83	0,12293536124298043	est. q: 82,87978901062161
q=84	0,2991527726039749	est. q: 83,86163474060595
q=85	0,28967514720920917	est. q: 84,9265405209735
q=86	0,1536722510470554	est. q: 85,8861057786271
q=87	0,3679949720606146	est. q: 86,910161934793
q=88	0,21875810842482224	est. q: 87,9468082333915
q=89	0,1949284416592718	est. q: 88,8877066783607
q=90	0,33585009744371064	est. q: 89,93042961228639
q=91	0,22816131872224332	est. q: 90,958133013919
q=92	0,15366335787598473	est. q: 91,89798114821315
q=93	0,4446088580157834	est. q: 92,90850972174667
q=94	0,1371350071192694	est. q: 93,93120513414033
q=95	0,258963805380489	est. q: 94,90930595784448
q=96	0,3289943299256919	est. q: 96,01558267313521
q=97	0,2387951076492314	est. q: 96,99368320580106
q=98	0,13732446557704137	est. q: 97,92957349563949
q=99	0,8949631033036098	est. q: 98,82904179685283
q=100	0	est. q: 100
Min variance: 0
Max variance: 0,8949631033036098

Those are implemented as disabled tests which outputs exactly these results so it can be checked in the future if for whatever reason estimator would be changed or when transition to 16-bit ints is done.

Summary
Max variances:
Luma - 5,001353712778048
Chroma - 0,8949631033036098

While Luma has max variance of ~5 I would argue that we should use it as threshold. It's way too large compared to other results where max value is ~2.4 and actual average is ~1.4. Especially considering 5 and 2.3 came from 99 and 98 quality settings respectively.

So my proposal for variance thresholds is:

Luma: 2,3629059983706604 (second max variance from tests)
Chroma 0,8949631033036098 (max variance from tests)

We can leave those values as they are even considering we store those tables as floats. Internally estimator uses casts to int so we should be fine.

Why quality < 25 isn't present in the tests?

Tests show that quality=25 is the lower confidence threshold, everything below it results is a >100 variance for both luma and chroma tables. This quality setting range should be considered highly compressed with a huge loss of details so we should threat them very carefully at re-encoding. Proposed variance thresholds above would force them to use original tables from decoded file (only if user didn't manually set quality setting in the encoder/metadata).

Copy link
Contributor

@gfoidl gfoidl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A micro-optimization hint.

Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To determine the variance thresholds, it seems worth to check how far do the variance values go with non-standard tables (I guess they are not slightly altered standard ones?). Maybe it's worth to keep the thresholds higher?

@antonfirsov
Copy link
Member

Looks like I was commenting on outdated code. Exposing LuminanceQuality + ChromaQuality + Quality is too much for a regular user IMO, can't we do some trick to avoid that?

@antonfirsov
Copy link
Member

@br3aker keeping LuminanceQuality and ChrominanceQuality JpegMetadata-only is much better, however I wonder if there is any end user use-case when a user is interested querying or altering them separately. If not, maybe we can turn them internal just like the quantization tables?

@br3aker
Copy link
Contributor Author

br3aker commented Jul 23, 2021

@antonfirsov I would like to say there is but I can't. There were no requests of such functionality. We could mimic JPEGsnoop functionality but JPEGsnoop already exists so yeah, it's better to hide everything and expose general Quality property for both metadata and encoder.

/// Note that jpeg image can have different quality for luminance and chrominance components.
/// This property return average for both qualities and sets both qualities to the given value.
/// </remarks>
public int Quality
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be a breaking change, but wouldn't it be better to define this as int? to better communicate the LuminanceQuality == null && LuminanceQuality == null case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, forgot about it being int? in current state. Will work on it tomorrow with no public luma/chroma in mind.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's public int Quality on the current master. I think we should consider changing it to int?, I doubt there are many users we would break.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. Break it. We cannot afford to hold onto properties that cause complexity. We simply don't have the maintainer numbers to manage continuously backwards compatible codebase.

I also vote max or we never really fix anything due to loss of detail/extra noise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got ya, thanks for your thoughts!

Copy link
Member

@antonfirsov antonfirsov Jul 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bit late to the party, since changes are already pushed but ...]

  1. Custom tables

We need to optimize to our most popular use case, which is thumbnail making. Assuming the user operation is a significant downscale, is the re-encoded thumbnail Jpeg "more correct" with the original tables or the default ones? I can't decide, such a downscale changes the frequency characteristics of the image a lot.

Sidenote: If I was to develop a thumbnail app, I would always set the Quality of the thumbnail to a higher value, even if it was low originally. Does ImageSharp.Web do anything like this @JimBobSquarePants?

  1. int or int?

To me metadata.Quality == null would represent the case that the decoder was unable to estimate the quality of the input image nicely, while returning metadata.Quality == 75 is quite an arbitrary choice. I vote for the breaking change (just let's make sure to apply the label).

Copy link
Contributor Author

@br3aker br3aker Jul 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to optimize to our most popular use case, which is thumbnail making. Assuming the user operation is a significant downscale, is the re-encoded thumbnail Jpeg "more correct" with the original tables or the default ones?

Barely noticable for 2x downsampling. So we have following situation:
Default tables:
good re-encoding with minimal change
good encoding of a heavily changed image

Original tables:
even better re-encoding with mininal change
good encoding of a heavily changed image

To me metadata.Quality == null would represent the case that the decoder was unable to estimate the quality of the input image nicely, while returning metadata.Quality == 75 is quite an arbitrary choice. I vote for the breaking change (just let's make sure to apply the label).

Not sure if we are talking about the same thing. metadata.Quality is not used during encoding process. Internal metadata.LuminanceQuality & metadata.ChrominanceQuality fields are used. Using quality = max(luma, chroma) for encoding would kill the quality if we are not talking about something extreme like thumbnails. Public quality is strictly a user way of knowing what was the decoded image quality. metadata.Quality setter sets luma & chroma to the same value. In other words, internal implementation won't really change and we either do (simplified, do that for both luma and chroma qualities):
int quality = encoder.Quality ?? metadata.Quality ?? 75; or int quality = encoder.Quality ?? metadata.Quality;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok @br3aker . After a thorough read of your quality determination logic I agree with your findings and find the refactoring of the various Quality properties to be good. The single metadata property information is useful for individuals who want to use a custom encoding quality based upon decoded information.

I don't think we should bother preserving the existing tables though. It's highly unlikely that individuals will decode an image for processing without significant change to the input pixels and most thumbnailing operations are closer to 10x reduction than 2x.

Copy link
Contributor Author

@br3aker br3aker Aug 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JimBobSquarePants thanks for the review! I might miss something but it's not breaking, no? We ended up leaving Quality property as int. Or it's all about two private properties in the metadata?

Agreed with table preservation. Did a bit of internal testing w/ and w/o using original tables, re-encoding without any modifications produces images with difference in size for about 0.1%. There is a binary difference but minutes of inspection with photoshop magnification tells that I can't spot it by eye.

What's still unresolved is quality < 25: new current implementation can't say for sure what actual quality jpeg has if it was encoded with quality less than 25. But I don't think many people would ever work with such jpegs and even if they would - they should save it as png to save as much detail as possible (or even legacy Jpeg XL re-encoding sometime in the future). So yep, tables bring a little to no difference, especially if any modification is involved.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@br3aker Ah sorry, my bad! I could have sworn I'd seen a change but I was wrong!

Thanks for doing the extra research. I agree with you on quality < 25. I really cannot see anyone doing that for anything other than experimental purpose.

@br3aker
Copy link
Contributor Author

br3aker commented Jul 27, 2021

Update

  • Removed quantization table carry-over in the metadata
  • Added tests where applicable
  • Used nint

I guess it's ready for final review.

@br3aker br3aker marked this pull request as ready for review July 28, 2021 02:05
@br3aker
Copy link
Contributor Author

br3aker commented Jul 28, 2021

@JimBobSquarePants as I suspected, langver 9.0 broke something, I'm just too newbie to determine what. Problem is so deeply nested and actually doesn't pop without nint commit which shouldn't actually break anything logic-wise.

At least we know for sure that we need to rollback to 8.0 or whatever version was used.

@JimBobSquarePants
Copy link
Member

@br3aker Sorry about all this flip-flopping on this PR. I'll do a proper sit-down review ASAP for you!

That nint thing is odd. I'll see what I can figure out!

@br3aker
Copy link
Contributor Author

br3aker commented Jul 31, 2021

@JimBobSquarePants having my code reviewed by .net professionals is worth the wait. I understand that you have a lot of work aside this project so no worries, take your time!

About that nint thingy - only x86 net472 failed, x64 and other runtimes/platforms are all good, I couldn't really find any relevant info about 9.0 langver being supported by it so maybe it's not. As we have a lot places with Unsafe.Add which all can be 'optimized' with nint usage I suggest to just forget about native int in this PR and open separate issue/PR for nint stuff and experiment with it there.

@JimBobSquarePants
Copy link
Member

.net professionals

Haha. No pressure!

I'll get on this tomorrow night.

Copy link
Member

@JimBobSquarePants JimBobSquarePants left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@br3aker If you're happy with this then I'm happy. Thanks again for your help and patience!

/// Note that jpeg image can have different quality for luminance and chrominance components.
/// This property return average for both qualities and sets both qualities to the given value.
/// </remarks>
public int Quality
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok @br3aker . After a thorough read of your quality determination logic I agree with your findings and find the refactoring of the various Quality properties to be good. The single metadata property information is useful for individuals who want to use a custom encoding quality based upon decoded information.

I don't think we should bother preserving the existing tables though. It's highly unlikely that individuals will decode an image for processing without significant change to the input pixels and most thumbnailing operations are closer to 10x reduction than 2x.

@JimBobSquarePants JimBobSquarePants added the breaking Signifies a binary breaking change. label Aug 4, 2021
@JimBobSquarePants JimBobSquarePants added this to the 1.1.0 milestone Aug 4, 2021
@JimBobSquarePants JimBobSquarePants removed the breaking Signifies a binary breaking change. label Aug 4, 2021
@JimBobSquarePants JimBobSquarePants merged commit 83427f2 into SixLabors:master Aug 4, 2021
@br3aker br3aker deleted the jpeg-quantization-metadata branch August 6, 2021 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use decoded quantization tables for Jpeg encoding, store them in JpegMetadata

5 participants