Feature: Support denorm=True in fig_rgb
Here is the current code from repr_rgb.py:
if denorm:
means = np.array(denorm[0])
stds = np.array(denorm[1])
x = x * stds + means
It would be helpful I think if checking denorm is True makes the default ImageNet values get used for the normalization:
((0.485, 0.456, 0.406), # mean
(0.229, 0.224, 0.225)) # std
This way during debugging (while moving in and out of many functions/scopes) you don't need to constantly worry about the denormalization stats being defined and available all the time. Just:
x.rgb(denorm=True).fig # OR...
x.rgb(True).fig
Alternatively a config could be used to store the default normalization stats to use for denorm=True. Custom normalization stats can still be passed exactly as before using e.g. .rgb(in_stats).
Heya. Not opposed to this. Can we do denorm="imagenet" denorm="symmetric" for when the input is scaled to [-1, 1], which is a pretty common normalization these days?
Using string values makes sense. Being able to set a config for the default value (string or tuple of tuples, used for x.rgb(True)) would then avoid excessive repetitions, and allow quick changing of normalization strategies program-wide if required.
I don't think a config variable is a good idea. You probably want to normalize something one way, and other things another way, and if your code is using some library that uses lovely numpy, now this library starts to normalize stuff incorrectly.
Let's just stick with the 2 common ones, imagenet and symmetric?
Also, maybe "auto" that looks at the input range and remaps the data to [0, 1] proportionally? It won't look right, but it will probably look ok for most images? IDK if it's a good idea, so we can stick with the first 2 for now?
Okay, imagenet and symmetric. I had a project in the past where I was calculating some filters on the inputs (Sobel, Laplacian, etc) and had to do something like auto in order to visualize them, so I think the use case is there. I would maybe not call it auto though, because that might connotate that it is automatically choosing between imagenet and symmetric. How about 'range' or 'minmax' or something like that instead?
minmax sounds good. My only concern - if we have one very large outlier, the image will appear all black or very dark. But i don't see a universal solution. We could rescale to put 95% of the values between 0 and 1, but that's a fragile heuristic. I think just plain minmax is good enough.
Yes, an outlier can be a problem, but at least 'minmax' is still doing exactly what it promises... (the 95% thing could be a bit less transparent to a user)
With minmax you also have the slight issue what if all the values are exactly the same (e.g. an originally pure red image prior to whatever normalization)? You need to avoid the division by zero.
The natural formulation would be (x - min) / (max - min) right? (min maps to 0, max maps to 1) To avoid the division by zero we could do (x - min) / (max - min + eps). This is a strategy often used throughout PyTorch for normalizations.
So this could be fine, but for very nearly constant values this has a bias towards outputting values closer to 0, so noting that: (x - min) / (max - min) = 0.5 + (x - 0.5 * (max + min)) / (max - min) we can instead just apply the eps to this latter equivalent formulation: 0.5 + (x - 0.5 * (max + min)) / (max - min + eps) This ensures that very nearly constant values end up centered around 0.5, not biased towards 0.
same value -> 0.5 sounds like a sane approach. I would not be opposed to just checking for min=max explicitly and settings the values to 0.5 if it makes the code simpler.
max and min and eps are just scalars, so I don't think the final expression above is so bad
In any case, the expression was intended to catch situations where min != max, but instead something like max - min ~= eps (e.g. floating point noise or whatever). In those situations the result will be centered around 0.5 and it won't happen that floating point noise decides whether value ends up completely at 0 or completely at 1. That was the secondary idea of the eps at least.
Sounds good to me.
@pallgeuer any eta on this? I want to recoed a video on lovely-*, and would love to cover this feature.
Okay, maybe a miscommunication. I didn't realise you wanted me to perform the actual implementation then. I never work with notebooks and haven't quite figured out this nbdev ecosystem/pipeline, so it would take me a bit to work out what to do. The previous pull request I made for another issue was like 3 lines, so I manually inserted those exact same lines in the notebook via a direct text edit to avoid the notebook/nbdev entirely.
If you want to implement it, here's the rundown:
Open notebooks in vscode, have the jupyter extension installed. Make sure you have nbdev installed too (dev dependency).
pip install -e . - so when you import in the notebook, it imports from the current dir/lovely_numpy
The way it works, cells in the notebooks marked with #| export go into the .py files. Note that when you from lovely_numpy.x import x in the noteook, it imports from the generated .py files - not the other notebooks.
There is a line at the beginning of each notebook import nbdev; nbdev.nbdev_export(). This runs nbdev_export, which converts all notebooks (not just the current one) to .py, so you don't need to call nbdev_export manually.
I often make use of restart kernel, run all cells, run all cells above - it's useful to assign hotkeys to them. For me: Crl-Shift-R - restart kernel Ctrl-Shirt-A - tun all cells Ctrl-Shift-Q - tun all above Ctrl-Shoft-C - clear all outputs.
Hack Hack Hack.
The norebooks naturally combine the code, the documentation and even the tests. The tests are pretty much just executed as part of the notebook. There is nbdev_test whch will execute all notebooks (without changing the output / exec counters).
Some other cell magics:
#| default_exp file.name -> export to lovely_numpy/file/name.py #| hide - don't show the cell in the documentation. #| eval: false - skip cell when testing.
There is also one special notebook - index.ipynb, which becomes README.md and the root of the html documentation. But otherwise it works the same way. It's not exported to .py though.
After you are done, before commit, run nbdev_prepate - it will run nbdev_export, clean (removes volatile matadata) and test on all files.
You can also do nbdev_preview - this generates the docs and you can preview them in the browser. The doc website xl0.github.io/lovely-numpy is generated with GH actions.
I'll have a look into this over the weekend