{"id":103,"date":"2024-02-06T12:13:45","date_gmt":"2024-02-06T12:13:45","guid":{"rendered":"https:\/\/www.photometric.io\/blog\/?p=103"},"modified":"2024-02-15T15:23:20","modified_gmt":"2024-02-15T15:23:20","slug":"realistic-image-degradation","status":"publish","type":"post","link":"https:\/\/www.photometric.io\/blog\/realistic-image-degradation\/","title":{"rendered":"Realistic Image Degradation"},"content":{"rendered":"\n<p>The qualitative performance of neural networks relies on how well the training input data matches the real data encountered during inference. Especially networks focusing on image denoising, deblurring or superresolution will heavily depend on small scale information on the pixel level. A typical pipeline for generating synthetic training data for those tasks is to start out with a high quality image, degrade it and use that one as the input during training.<\/p>\n\n\n\n<p>In this blog post I want to introduce my pipeline for taking a perfectly fine image and making it ugly. BUT in a realistic way.<\/p>\n\n\n\n<p>The following explanation probably leaves out a few steps, but I&#8217;ve tried to link to the Wikipedia page of all technical terms and the source code using pytorch is available on the <a href=\"https:\/\/github.com\/nhauber99\/degradr\" target=\"_blank\" rel=\"noopener\">&#8220;degradr&#8221; GitHub repository<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-x-large-font-size\">The Theory<\/h2>\n\n\n\n<p>To start out imagine a photo before it even hit your lens. It&#8217;s perfect, no blur, no chromatic abberation, no JPEG artifacts and <s>no noise<\/s>. Well tough luck, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Shot_noise\" target=\"_blank\" rel=\"noopener\">shot noise<\/a> is everywhere. It then gets refracted by your lens and focused onto the sensor. Along the way several <a href=\"https:\/\/en.wikipedia.org\/wiki\/Optical_aberration\" target=\"_blank\" rel=\"noopener\">optical aberrations<\/a> start to degrade your image. Then the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Color_filter_array\" target=\"_blank\" rel=\"noopener\">color filter array (CFA)<\/a> of your camera filters out light of certain wavelengths. The remaining light hits your sensor causing <a href=\"https:\/\/en.wikipedia.org\/wiki\/Photoelectric_effect\" target=\"_blank\" rel=\"noopener\">photoelectrons<\/a> to be emmited. At the end of the exposure all photoelectrons of a pixel get amplified according to the camera gain and then the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Analog-to-digital_converter\" target=\"_blank\" rel=\"noopener\">ADC<\/a> converts those into data numbers (DN). This step introduces both read noise and quantization noise. <\/p>\n\n\n\n<p>Now you have a digital image, but probably not one you&#8217;d want to look at just yet. The camera white balance still needs to be applied followed by <a href=\"https:\/\/en.wikipedia.org\/wiki\/Demosaicing\" target=\"_blank\" rel=\"noopener\">demosaicing<\/a> and a color space transformation to the color space of your choice. If you don&#8217;t shoot RAW, your camera will even compress your image to a JPEG file.<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-x-large-font-size\">Implementation<\/h2>\n\n\n\n<p>Coming up: how to simulate all that fast enough to be done live on the CPU while training a neural network.<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-large-font-size\">Optical Abberations<\/h2>\n\n\n\n<p>The first and probably more complex step is to simulate the optical abberations. A fast way to do this is to convolve the image with a color dependent <a href=\"https:\/\/en.wikipedia.org\/wiki\/Point_spread_function\" target=\"_blank\" rel=\"noopener\">point spread function (PSF)<\/a>. This PSF should be as realistic as possible and fortunately devices exist for measuring this for a given lens. Unfortunately I&#8217;m not willing to spend a &#8220;request a quote&#8221; amount of money on one, so we&#8217;ll just approximate instead. For generating semi-realistic PSFs I&#8217;ve used the <a href=\"https:\/\/github.com\/brandondube\/prysm\" target=\"_blank\" rel=\"noopener\">prysm<\/a> library in combination with random <a href=\"https:\/\/en.wikipedia.org\/wiki\/Zernike_polynomials\" target=\"_blank\" rel=\"noopener\">zernike polynomials<\/a>, these need to be generated as a preprocessing step before training, the convolution can be done live. Here are some examples:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full is-resized\"><img decoding=\"async\" width=\"382\" height=\"128\" src=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/01\/zernike_psf.jpg\" alt=\"\" class=\"wp-image-109\" style=\"width:513px;height:auto\" srcset=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/01\/zernike_psf.jpg 382w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/01\/zernike_psf-300x101.jpg 300w\" sizes=\"(max-width: 382px) 100vw, 382px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading has-large-font-size\">CFA<\/h2>\n\n\n\n<p>After being refracted through the lens the light hits an antialiasing filter which introduces some more blur followed by the color filter array. Simulating that is easy: we convert the color image into a monochrome one and for each pixel we set the value to either the red, green or blue channel according to the specified pattern.<\/p>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-layout-1 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:100%\">\n<figure class=\"wp-block-image aligncenter size-large is-resized\"><img decoding=\"async\" width=\"1024\" height=\"666\" src=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/01\/image-1-1024x666.png\" alt=\"\" class=\"wp-image-29\" style=\"width:404px;height:auto\" srcset=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/01\/image-1-1024x666.png 1024w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/01\/image-1-300x195.png 300w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/01\/image-1-768x499.png 768w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/01\/image-1-1536x998.png 1536w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/01\/image-1.png 1920w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Bayer Pattern<br>Image from <a href=\"https:\/\/commons.wikimedia.org\/wiki\/File:Bayer_pattern_on_sensor.svg\" target=\"_blank\" rel=\"noopener\">Wikipedia<\/a> under the <a href=\"https:\/\/creativecommons.org\/licenses\/by-sa\/3.0\/deed.en\" target=\"_blank\" rel=\"noopener\">CC BY-SA 3.0 License<\/a><\/figcaption><\/figure>\n<\/div>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading has-large-font-size\">Noise<\/h2>\n\n\n\n<p>Now is the time for the different types of noise. First we apply the shot noise, which is simply a poisson distribution. Then we multiply the whole image by the gain followed by adding read noise which is modelled via two gaussian distributions. Keep in mind that in reality the amount of read noise will also depend on the selected gain, the exact relationship varies for different camera models. Lastly we simulate the quantization noise by rounding all values.<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-large-font-size\">Camera White Balance<\/h2>\n\n\n\n<p>Given that the filters of the CFA have different sensivities, all pixels need to be multiplied by a certain factor depending on the color of their respective filter in order to correct this imbalance.<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-large-font-size\">Demosaicing<\/h2>\n\n\n\n<p>Several different algorithms exist for demosaicing, three of those are implemented in the <a href=\"https:\/\/www.intel.com\/content\/www\/us\/en\/developer\/tools\/oneapi\/ipp.html\" target=\"_blank\" rel=\"noopener\">Intel Integrated Performance Primitives<\/a>. Unfortunately there isn&#8217;t an available python wrapper for this, so I wrote one myself. The usage is as follows: pass in the monochrome image with the bayer pattern and get a color image back. Depending on the throughput of your network as well as your CPU, this can also be done live, but be aware that the AHD demosaicing is significantly slower than the other methods.<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-large-font-size\">Color Space Transformation<\/h2>\n\n\n\n<p>Until now the image is still in the camera color space and would look weird if we display it on a monitor intended for a different color space like sRGB. The color space transformation is luckily very simple, we only have to multiply each rgb pixel (assumed as a vector) by the 3&#215;3 transformation matrix. A list of realistic transformation matrices can be found in the <a href=\"https:\/\/github.com\/LibRaw\/LibRaw\" target=\"_blank\" rel=\"noopener\">LibRaw<\/a> library.<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-large-font-size\">JPEG Artifacts<\/h2>\n\n\n\n<p>Finally, if we want our neural network to be also trained on handling JPEG artifacts, we of course have to add those as well. This is implemented via a call to the imageio library.<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-x-large-font-size\">Results<\/h2>\n\n\n\n<p>Here you can see an example of the library being tested on a small crop of an image of the <a href=\"https:\/\/www.reddit.com\/media?url=https%3A%2F%2Fi.redd.it%2F1bro7abyovw81.jpg\" target=\"_blank\" rel=\"noopener\">Heart Nebula<\/a> I took a while ago. (click to expand)<\/p>\n\n\n\n<figure data-wp-context=\"{ &quot;core&quot;:\n\t\t\t\t{ &quot;image&quot;:\n\t\t\t\t\t{   &quot;imageLoaded&quot;: false,\n\t\t\t\t\t\t&quot;initialized&quot;: false,\n\t\t\t\t\t\t&quot;lightboxEnabled&quot;: false,\n\t\t\t\t\t\t&quot;hideAnimationEnabled&quot;: false,\n\t\t\t\t\t\t&quot;preloadInitialized&quot;: false,\n\t\t\t\t\t\t&quot;lightboxAnimation&quot;: &quot;zoom&quot;,\n\t\t\t\t\t\t&quot;imageUploadedSrc&quot;: &quot;https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/02\/image_degrade.jpg&quot;,\n\t\t\t\t\t\t&quot;imageCurrentSrc&quot;: &quot;&quot;,\n\t\t\t\t\t\t&quot;targetWidth&quot;: &quot;1536&quot;,\n\t\t\t\t\t\t&quot;targetHeight&quot;: &quot;317&quot;,\n\t\t\t\t\t\t&quot;scaleAttr&quot;: &quot;&quot;,\n\t\t\t\t\t\t&quot;dialogLabel&quot;: &quot;Enlarged image&quot;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\" data-wp-interactive class=\"wp-block-image size-large wp-lightbox-container\"><img decoding=\"async\" width=\"1024\" height=\"211\" data-wp-effect--setStylesOnResize=\"effects.core.image.setStylesOnResize\" data-wp-effect=\"effects.core.image.setButtonStyles\" data-wp-init=\"effects.core.image.initOriginImage\" data-wp-on--click=\"actions.core.image.showLightbox\" data-wp-on--load=\"actions.core.image.handleLoad\" src=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/02\/image_degrade-1024x211.jpg\" alt=\"example of the simulated image degradation\" class=\"wp-image-115\" srcset=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/02\/image_degrade-1024x211.jpg 1024w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/02\/image_degrade-300x62.jpg 300w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/02\/image_degrade-768x159.jpg 768w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/02\/image_degrade.jpg 1536w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Enlarge image: example of the simulated image degradation\"\n\t\t\tdata-wp-on--click=\"actions.core.image.showLightbox\"\n\t\t\tdata-wp-style--right=\"context.core.image.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"context.core.image.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button>        <div data-wp-body=\"\" class=\"wp-lightbox-overlay zoom\"\n            data-wp-bind--role=\"selectors.core.image.roleAttribute\"\n            data-wp-bind--aria-label=\"selectors.core.image.dialogLabel\"\n            data-wp-class--initialized=\"context.core.image.initialized\"\n            data-wp-class--active=\"context.core.image.lightboxEnabled\"\n            data-wp-class--hideAnimationEnabled=\"context.core.image.hideAnimationEnabled\"\n            data-wp-bind--aria-modal=\"selectors.core.image.ariaModal\"\n            data-wp-effect=\"effects.core.image.initLightbox\"\n            data-wp-on--keydown=\"actions.core.image.handleKeydown\"\n            data-wp-on--touchstart=\"actions.core.image.handleTouchStart\"\n            data-wp-on--touchmove=\"actions.core.image.handleTouchMove\"\n            data-wp-on--touchend=\"actions.core.image.handleTouchEnd\"\n            data-wp-on--click=\"actions.core.image.hideLightbox\"\n            tabindex=\"-1\"\n            >\n                <button type=\"button\" aria-label=\"Close\" style=\"fill: var(--wp--preset--color--foreground)\" class=\"close-button\" data-wp-on--click=\"actions.core.image.hideLightbox\">\n                    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z\"><\/path><\/svg>\n                <\/button>\n                <div class=\"lightbox-image-container\">\n<figure class=\"wp-block-image size-large responsive-image\"><img decoding=\"async\" data-wp-bind--src=\"context.core.image.imageCurrentSrc\" data-wp-style--object-fit=\"selectors.core.image.lightboxObjectFit\" src=\"\" alt=\"example of the simulated image degradation\" class=\"wp-image-115\"\/><\/figure>\n<\/div>\n                <div class=\"lightbox-image-container\">\n<figure class=\"wp-block-image size-large enlarged-image\"><img decoding=\"async\" data-wp-bind--src=\"selectors.core.image.enlargedImgSrc\" data-wp-style--object-fit=\"selectors.core.image.lightboxObjectFit\" src=\"\" alt=\"example of the simulated image degradation\" class=\"wp-image-115\"\/><\/figure>\n<\/div>\n                <div class=\"scrim\" style=\"background-color: var(--wp--preset--color--background)\" aria-hidden=\"true\"><\/div>\n        <\/div><\/figure>\n\n\n\n<p>In summary, first the input is blurred by a random PSF which introduces blur and chromatic abberation. Then noise is added followed by the CFA and demosaicing. Note that the order of Noise and CFA doesn&#8217;t matter for the implementation and this way it&#8217;s easier to see the impact the CFA+Demosaic has on the noise distribution on a pixel scale. Lastly JPEG compression is applied further increasing the image degradation.<\/p>\n\n\n\n<p>The python library is available here: <a href=\"https:\/\/github.com\/nhauber99\/degradr\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/nhauber99\/degradr<\/a><\/p>\n\n\n\n<p>So far I&#8217;ve only tested it for one machine learning project which is still under development, but I plan on running some tests with readily available neural networks like <a href=\"https:\/\/github.com\/xinntao\/ESRGAN\" target=\"_blank\" rel=\"noopener\">ESRGAN<\/a> to make some comparisons with the previously used preprocessing steps. <\/p>\n","protected":false},"excerpt":{"rendered":"<p>The qualitative performance of neural networks relies on how well the training input data matches the real data encountered during inference. Especially networks focusing on image denoising, deblurring or superresolution will heavily depend on small scale information on the pixel level. A typical pipeline for generating synthetic training data for those tasks is to start [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[14,16],"tags":[8,7,17,6,18],"_links":{"self":[{"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/posts\/103"}],"collection":[{"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/comments?post=103"}],"version-history":[{"count":22,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/posts\/103\/revisions"}],"predecessor-version":[{"id":279,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/posts\/103\/revisions\/279"}],"wp:attachment":[{"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/media?parent=103"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/categories?post=103"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/tags?post=103"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}