├── tmp └── __keep__ ├── tests ├── __init__.py ├── test_debayer.py ├── test_utils.py └── test_metrics.py ├── debayer ├── apps │ ├── __init__.py │ ├── example.py │ ├── utils.py │ ├── compare.py │ ├── eval.py │ └── benchmark.py ├── __version__.py ├── __init__.py ├── layouts.py ├── utils.py ├── metrics.py └── modules.py ├── requirements ├── core.txt ├── dev.txt └── apps.txt ├── etc ├── test.bmp └── readme │ ├── input.png │ ├── test-mosaic-l588-r817-b1178-t981.png │ ├── test-mosaic-l1429-r1659-b1889-t1725.png │ ├── test-mosaic-l1779-r1998-b1145-t949.png │ └── test-mosaic-l620-r872-b1430-t1233.png ├── LICENSE ├── setup.py ├── .github └── workflows │ └── python-package.yml ├── Metrics.md ├── .gitignore ├── Readme.md └── Benchmarks.md /tmp/__keep__: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debayer/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debayer/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.4.1" 2 | -------------------------------------------------------------------------------- /requirements/core.txt: -------------------------------------------------------------------------------- 1 | torch>=1.3 2 | numpy>=1.20 -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | black 3 | flake8 -------------------------------------------------------------------------------- /etc/test.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheind/pytorch-debayer/HEAD/etc/test.bmp -------------------------------------------------------------------------------- /etc/readme/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheind/pytorch-debayer/HEAD/etc/readme/input.png -------------------------------------------------------------------------------- /requirements/apps.txt: -------------------------------------------------------------------------------- 1 | matplotlib>=3.4.2 2 | py-cpuinfo>=8.0.0 3 | pandas>=1.3.2 4 | opencv-python>=4.0 5 | -------------------------------------------------------------------------------- /etc/readme/test-mosaic-l588-r817-b1178-t981.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheind/pytorch-debayer/HEAD/etc/readme/test-mosaic-l588-r817-b1178-t981.png -------------------------------------------------------------------------------- /etc/readme/test-mosaic-l1429-r1659-b1889-t1725.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheind/pytorch-debayer/HEAD/etc/readme/test-mosaic-l1429-r1659-b1889-t1725.png -------------------------------------------------------------------------------- /etc/readme/test-mosaic-l1779-r1998-b1145-t949.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheind/pytorch-debayer/HEAD/etc/readme/test-mosaic-l1779-r1998-b1145-t949.png -------------------------------------------------------------------------------- /etc/readme/test-mosaic-l620-r872-b1430-t1233.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheind/pytorch-debayer/HEAD/etc/readme/test-mosaic-l620-r872-b1430-t1233.png -------------------------------------------------------------------------------- /debayer/__init__.py: -------------------------------------------------------------------------------- 1 | from debayer.layouts import Layout # noqa 2 | from debayer.modules import Debayer2x2, Debayer3x3, DebayerSplit, Debayer5x5 # noqa 3 | from . import utils # noqa 4 | from . import metrics # noqa 5 | from .__version__ import __version__ # noqa 6 | -------------------------------------------------------------------------------- /debayer/layouts.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class Layout(enum.Enum): 5 | """Possible Bayer color filter array layouts. 6 | 7 | The value of each entry is the color index (R=0,G=1,B=2) 8 | within a 2x2 Bayer block. 9 | """ 10 | 11 | RGGB = (0, 1, 1, 2) 12 | GRBG = (1, 0, 2, 1) 13 | GBRG = (1, 2, 0, 1) 14 | BGGR = (2, 1, 1, 0) 15 | -------------------------------------------------------------------------------- /debayer/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import numpy as np 4 | import torch 5 | 6 | from .layouts import Layout 7 | 8 | 9 | def rgb_to_bayer( 10 | x: Union[np.ndarray, torch.Tensor], layout=Layout.RGGB 11 | ) -> Union[np.ndarray, torch.Tensor]: 12 | """Converts an RGB image to Bayer image. 13 | 14 | Args: 15 | x: (H,W,C) RGB image 16 | layout: Layout to encode in 17 | 18 | Returns 19 | b: (H,W) Bayer image with expected layout 20 | """ 21 | if torch.is_tensor(x): 22 | C, H, W = x.shape 23 | p = torch.tensor(layout.value).reshape(2, 2) 24 | return torch.gather(x, 1, p.repeat(1, H // 2, W // 2)) 25 | else: 26 | H, W, C = x.shape 27 | p = np.array(layout.value).reshape(2, 2) 28 | pp = np.tile(p, (H // 2, W // 2)) 29 | b = np.take_along_axis(x, np.expand_dims(pp, -1), -1).squeeze(-1) 30 | return b 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christoph Heindl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from pathlib import Path 3 | 4 | THISDIR = Path(__file__).parent 5 | 6 | 7 | def read_requirements(fname): 8 | with open(THISDIR / "requirements" / fname, "r") as f: 9 | return f.read().splitlines() 10 | 11 | 12 | core_required = read_requirements("core.txt") 13 | apps_required = read_requirements("apps.txt") 14 | dev_required = read_requirements("dev.txt") + apps_required 15 | 16 | main_ns = {} 17 | with open(THISDIR / "debayer" / "__version__.py") as ver_file: 18 | exec(ver_file.read(), main_ns) 19 | 20 | setup( 21 | name="pytorch-debayer", 22 | version=main_ns["__version__"], 23 | description="Convolutional Debayering/Demosaicing layer for PyTorch", 24 | author="Christoph Heindl", 25 | url="https://github.com/cheind/pytorch-debayer/", 26 | license="MIT", 27 | install_requires=core_required, 28 | packages=find_packages(".", include="debayer*"), 29 | include_package_data=True, 30 | keywords="debayer bayer pytorch convolution", 31 | extras_require={ 32 | "dev": dev_required, 33 | "apps": apps_required, 34 | "full": core_required + dev_required, 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /tests/test_debayer.py: -------------------------------------------------------------------------------- 1 | import debayer 2 | import numpy as np 3 | import pytest 4 | import torch 5 | 6 | Colors = [ 7 | (1.0, 0.0, 0.0), 8 | (0.0, 1.0, 0.0), 9 | (0.0, 0.0, 1.0), 10 | ] 11 | 12 | 13 | def _bayer_to_torch(x: np.ndarray, dtype=torch.float32, dev: str = "cpu"): 14 | return torch.tensor(x).to(dtype).unsqueeze(0).unsqueeze(0).to(dev) 15 | 16 | 17 | @pytest.mark.parametrize("layout", debayer.Layout) 18 | @pytest.mark.parametrize("color", Colors) 19 | @pytest.mark.parametrize( 20 | "klass", [debayer.Debayer2x2, debayer.Debayer3x3, debayer.Debayer5x5] 21 | ) 22 | def test_monochromatic_images(layout, color, klass): 23 | """Algorithms should be able to reconstruct monochromatic bayer images.""" 24 | rgb = np.tile( 25 | np.array(color, dtype=np.float32).reshape(1, 1, -1), 26 | (6, 8, 1), 27 | ) 28 | b = _bayer_to_torch(debayer.utils.rgb_to_bayer(rgb, layout=layout)) 29 | r = klass(layout=layout)(b) 30 | 31 | # import matplotlib.pyplot as plt 32 | # plt.imshow(r.squeeze().permute(1, 2, 0).cpu().to(torch.float32).numpy()) 33 | # plt.show() 34 | assert r.shape == (1, 3, 6, 8) 35 | assert (r == torch.tensor(color).view(1, -1, 1, 1)).all() 36 | -------------------------------------------------------------------------------- /debayer/metrics.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import torch 4 | 5 | 6 | def peak_signal_noise_ratio( 7 | x: torch.tensor, y: torch.tensor, datarange: float 8 | ) -> Tuple[torch.FloatTensor, torch.BoolTensor]: 9 | """Computes the channel-wise peak signal to noise ratio (PSNR) for the given image(s). 10 | 11 | Computed according to 12 | psnr(x,y) = 10 * log10 (PEAK^2/MSE) 13 | = 20 * log10 (PEAK/MSE^0.5) 14 | = 20 * log10 PEAK - 10 * log10 MSE 15 | where PEAK is datarange. 16 | 17 | Args: 18 | x: (B,C,*) original image 19 | y: (B,C,*) reconstructed image 20 | datarange: range of values 21 | 22 | Returns 23 | psnr: (B,C) peak signal to noise ratios. 24 | mask: (B,C) boolean mask indicating no differnce. 25 | """ 26 | 27 | B, C = x.shape[:2] 28 | assert y.shape[:2] == x.shape[:2] 29 | 30 | x = x.reshape(B, C, -1).float() 31 | y = y.reshape(B, C, -1).float() 32 | 33 | mse = ((x - y) ** 2).mean(-1) # (B,C) 34 | # Sanity to return SNR values where there is no error 35 | mask = mse == 0.0 36 | mse[mask] = torch.finfo(x.dtype).tiny 37 | value = 20 * torch.log10(torch.as_tensor(datarange)) - 10 * torch.log10(mse) 38 | return value, mask 39 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import debayer 4 | 5 | 6 | def test_to_bayer(): 7 | rgb = np.random.rand(20, 30, 3) 8 | b = debayer.utils.rgb_to_bayer(rgb, layout=debayer.Layout.RGGB) 9 | assert b.shape == (20, 30) 10 | assert b[0, 0] == rgb[0, 0, 0] 11 | assert b[0, 1] == rgb[0, 1, 1] 12 | assert b[0, 2] == rgb[0, 2, 0] 13 | assert b[1, 2] == rgb[1, 2, 1] 14 | assert b[1, 3] == rgb[1, 3, 2] 15 | 16 | b = debayer.utils.rgb_to_bayer(rgb, layout=debayer.Layout.BGGR) 17 | assert b.shape == (20, 30) 18 | assert b[0, 0] == rgb[0, 0, 2] 19 | assert b[0, 1] == rgb[0, 1, 1] 20 | assert b[0, 2] == rgb[0, 2, 2] 21 | assert b[1, 2] == rgb[1, 2, 1] 22 | assert b[1, 3] == rgb[1, 3, 0] 23 | 24 | b = debayer.utils.rgb_to_bayer(rgb, layout=debayer.Layout.GBRG) 25 | assert b.shape == (20, 30) 26 | assert b[0, 0] == rgb[0, 0, 1] 27 | assert b[0, 1] == rgb[0, 1, 2] 28 | assert b[0, 2] == rgb[0, 2, 1] 29 | assert b[1, 2] == rgb[1, 2, 0] 30 | assert b[1, 3] == rgb[1, 3, 1] 31 | 32 | b = debayer.utils.rgb_to_bayer(rgb, layout=debayer.Layout.GRBG) 33 | assert b.shape == (20, 30) 34 | assert b[0, 0] == rgb[0, 0, 1] 35 | assert b[0, 1] == rgb[0, 1, 0] 36 | assert b[0, 2] == rgb[0, 2, 1] 37 | assert b[1, 2] == rgb[1, 2, 2] 38 | assert b[1, 3] == rgb[1, 3, 1] 39 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | python -m pip install .[full] 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | pytest 41 | -------------------------------------------------------------------------------- /Metrics.md: -------------------------------------------------------------------------------- 1 | # Additional Evaluation Results 2 | 3 | Metrics are evaluated on different full-color datasets such as 4 | - Kodak 5 | http://r0k.us/graphics/kodak/ 6 | https://github.com/MohamedBakrAli/Kodak-Lossless-True-Color-Image-Suite 7 | - McMaster 8 | https://www4.comp.polyu.edu.hk/~cslzhang/CDM_Dataset.htm 9 | 10 | To run the evaluation, make each dataset a folder and run 11 | ``` 12 | python -m debayer.apps.eval --methods all 13 | ``` 14 | 15 | Note, in case the latest version below does not match the current version of pytorch-debayer, performance statistics to remain unchanged. 16 | 17 | ## Version 1.4.0 18 | 19 | ### PSNR 20 | The PSNR (Peak-Signal-Noise-Ratio) values (dB, higher is better) of each channel (R, G, B) and PSNR of the whole image (RGB) across 2 Datasets (Kodak, McMaster) and for each algorithm. 21 | 22 | | Database | Method | R (dB) | G (dB) | B (dB) | PSNR (dB) | 23 | |------------|--------------|-------|-------|-------|--------| 24 | | Kodak | Debayer2x2 | 26.64 | 28.18 | 26.98 | 27.27 | 25 | | | Debayer3x3 | 28.18 | 32.66 | 28.86 | 29.90 | 26 | | | Debayer5x5 | 33.84 | 38.05 | 33.53 | 35.14 | 27 | | | DebayerSplit | 26.64 | 32.66 | 26.98 | 28.76 | 28 | | | OpenCV | 28.15 | 31.25 | 28.62 | 29.34 | 29 | | McMaster | Debayer2x2 | 28.47 | 30.32 | 28.63 | 29.14 | 30 | | | Debayer3x3 | 31.68 | 35.40 | 31.25 | 32.78 | 31 | | | Debayer5x5 | 34.04 | 37.62 | 33.02 | 34.89 | 32 | | | DebayerSplit | 28.47 | 35.40 | 28.63 | 30.83 | 33 | | | OpenCV | 31.64 | 35.22 | 31.22 | 32.69 | -------------------------------------------------------------------------------- /tests/test_metrics.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import math 3 | import cv2 4 | 5 | from debayer.metrics import peak_signal_noise_ratio 6 | 7 | 8 | def test_psnr(): 9 | o = torch.randint(0, 255, (1, 3, 200, 300)).float() 10 | r = o.clone() 11 | 12 | # Assert all equal 13 | psnr, equal_mask = peak_signal_noise_ratio(o, r, 255.0) 14 | assert equal_mask.all() # 15 | 16 | # Assert non equal and correct shapes 17 | r = r + torch.rand_like(o) * 1e-3 18 | psnr, equal_mask = peak_signal_noise_ratio(o, r, 255.0) 19 | assert (~equal_mask).all() # 20 | assert equal_mask.shape == (1, 3) 21 | assert psnr.shape == (1, 3) 22 | 23 | # Assert the mean PSNR over channels is the same as for a single channel input 24 | assert torch.isclose( 25 | psnr.mean().view(1, 1), 26 | peak_signal_noise_ratio(o.view(1, 1, -1), r.view(1, 1, -1), 255)[0], 27 | atol=1e-4, 28 | ) 29 | 30 | # Assert values are correct 31 | psnr, _ = peak_signal_noise_ratio( 32 | torch.tensor([10.0]).view(1, 1, 1), torch.tensor([8.0]).view(1, 1, 1), 10.0 33 | ) 34 | assert torch.isclose(psnr, torch.tensor(20 - 10 * math.log10(4))) 35 | # Assert values are correct (normalized) 36 | psnr, _ = peak_signal_noise_ratio( 37 | torch.tensor([1.0]).view(1, 1, 1), torch.tensor([8.0 / 10.0]).view(1, 1, 1), 1.0 38 | ) 39 | assert torch.isclose(psnr, torch.tensor(20 - 10 * math.log10(4))) 40 | 41 | # Compare to OpenCV 42 | o = torch.randint(0, 255, size=(3, 200, 300), dtype=torch.uint8) 43 | r = torch.randint(0, 255, size=(3, 200, 300), dtype=torch.uint8) 44 | 45 | psnr, _ = peak_signal_noise_ratio(o.unsqueeze(0), r.unsqueeze(0), 255.0) 46 | psnr_cv = cv2.PSNR(o.permute(1, 2, 0).numpy(), r.permute(1, 2, 0).numpy()) 47 | torch.isclose(psnr, torch.as_tensor(psnr_cv), atol=1e-3) 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | tmp/ 132 | !tmp/__keep__ -------------------------------------------------------------------------------- /debayer/apps/example.py: -------------------------------------------------------------------------------- 1 | """Demonstrates demosaicing of a single image using pytorch-debayer. 2 | 3 | Per default the script imports an image and interprets it as Bayer 4 | image with layout according to `--layout` parameter. In case the input 5 | image is full color image, use `--full-color` switch to convert the image 6 | first to a simulated Bayer image. 7 | """ 8 | 9 | import argparse 10 | import logging 11 | from pathlib import Path 12 | 13 | import debayer 14 | import matplotlib.pyplot as plt 15 | import torch 16 | 17 | from . import utils 18 | 19 | 20 | @torch.no_grad() 21 | def main(): 22 | logging.basicConfig(level=logging.INFO) 23 | parser = argparse.ArgumentParser() 24 | parser.add_argument( 25 | "--method", 26 | choices=["Debayer2x2", "Debayer3x3", "Debayer5x5", "DebayerSplit"], 27 | default="Debayer5x5", 28 | help="Debayer algorithm to use.", 29 | ) 30 | parser.add_argument( 31 | "--layout", 32 | type=debayer.Layout, 33 | choices=list(debayer.Layout), 34 | default=debayer.Layout.RGGB, 35 | help="Bayer layout of Bayer input image. Only applicable if --full-color is omitted", # noqa 36 | ) 37 | parser.add_argument("--half", action="store_true", help="Use 16bit fp precision") 38 | parser.add_argument( 39 | "--bayer", 40 | action="store_true", 41 | help="If input image is multi-channel, assume encoding is Bayer", 42 | ) 43 | parser.add_argument("--dev", default="cuda") 44 | parser.add_argument("image") 45 | args = parser.parse_args() 46 | 47 | # Read Bayer image 48 | input_image, b = utils.read_image(args.image, bayer=args.bayer, layout=args.layout) 49 | 50 | # Setup precision 51 | prec = torch.float16 if args.half else torch.float32 52 | 53 | # Init filter 54 | deb = { 55 | "Debayer2x2": debayer.Debayer2x2, 56 | "Debayer3x3": debayer.Debayer3x3, 57 | "Debayer5x5": debayer.Debayer5x5, 58 | "DebayerSplit": debayer.DebayerSplit, 59 | }[args.method](layout=args.layout) 60 | deb = deb.to(args.dev).to(prec) 61 | 62 | # Prepare input with shape Bx1xHxW and 63 | t = (torch.tensor(b).to(prec).unsqueeze(0).unsqueeze(0).to(args.dev)) / 255.0 64 | 65 | # Compute and move back to CPU 66 | rgb = deb(t).squeeze().permute(1, 2, 0).cpu().to(torch.float32).numpy() 67 | 68 | fig = utils.create_mosaic( 69 | [input_image, rgb], 70 | roi=(0, rgb.shape[1], 0, rgb.shape[0]), 71 | labels=["Bayer", args.method], 72 | ) 73 | fig.savefig(f"tmp/{Path(args.image).with_suffix('.png').name}") 74 | plt.show() 75 | 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /debayer/apps/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Tuple, List 3 | import matplotlib.gridspec as gridspec 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from PIL import Image 7 | 8 | import debayer 9 | 10 | _logger = logging.getLogger("debayer") 11 | 12 | 13 | def read_image( 14 | path: str, 15 | bayer: bool, 16 | layout: debayer.Layout = debayer.Layout.RGGB, 17 | loglevel: int = logging.INFO, 18 | ) -> Tuple[np.ndarray, np.ndarray]: 19 | x: np.ndarray = np.asarray(Image.open(path)) 20 | if x.ndim > 2 and not bayer: 21 | _logger.log( 22 | loglevel, 23 | "Loading multi-channel input as RGB image. " 24 | "Use `--bayer` to force Bayer interpretation and `--layout` " 25 | "to specify its layout.", 26 | ) 27 | _logger.log(loglevel, f"Converting RGB to Bayer image with layout {layout}.") 28 | # Consider full color, convert to bayer 29 | b = debayer.utils.rgb_to_bayer(x[..., :3], layout=layout) 30 | elif x.ndim > 2 and bayer: 31 | _logger.log( 32 | loglevel, 33 | f"Loading multi-channel input as Bayer {layout} image. " 34 | "Omit `--bayer` to force RGB interpretation. ", 35 | ) 36 | b = x[..., 0].copy() 37 | else: 38 | b = x.copy() 39 | _logger.log( 40 | loglevel, 41 | f"Interpreting single-channel input as Bayer {layout} image. " 42 | "Use `--layout` to specify its layout.", 43 | ) 44 | return x, b 45 | 46 | 47 | def opencv_conversion_code(layout: debayer.Layout): 48 | import cv2 49 | 50 | return { 51 | layout.RGGB: cv2.COLOR_BAYER_BG2RGB, 52 | layout.BGGR: cv2.COLOR_BAYER_RG2RGB, 53 | layout.GBRG: cv2.COLOR_BAYER_GR2RGB, 54 | layout.GRBG: cv2.COLOR_BAYER_GB2RGB, 55 | }[layout] 56 | 57 | 58 | def create_mosaic( 59 | imgs: List[np.ndarray], roi: Tuple[int, int, int, int], labels: List[str] 60 | ): 61 | width = roi[1] - roi[0] 62 | height = roi[3] - roi[2] 63 | 64 | N = len(imgs) 65 | W, H = plt.figaspect(height / width) 66 | 67 | fig = plt.figure(constrained_layout=True, figsize=(W * len(imgs), H), frameon=False) 68 | nrow = 1 69 | ncol = N 70 | spec = gridspec.GridSpec( 71 | nrow, 72 | ncol, 73 | wspace=0.0, 74 | hspace=0.0, 75 | top=1.0 - 0.5 / (nrow + 1), 76 | bottom=0.5 / (nrow + 1), 77 | left=0.5 / (ncol + 1), 78 | right=1 - 0.5 / (ncol + 1), 79 | figure=fig, 80 | ) 81 | 82 | def plot_image(idx, row, col, shareax=None): 83 | cmap = "gray" if imgs[idx].ndim == 2 else None 84 | ax = fig.add_subplot(spec[row, col], sharey=shareax, sharex=shareax) 85 | ax.imshow(imgs[idx], interpolation="nearest", origin="upper", cmap=cmap) 86 | ax.set_title(labels[idx], size=10, y=1.0, pad=-14, color="white") 87 | ax.tick_params( 88 | bottom=False, 89 | top=False, 90 | left=False, 91 | right=False, 92 | labelleft=False, 93 | labelbottom=False, 94 | ) 95 | ax.set_xlim(roi[0], roi[1]) 96 | ax.set_ylim(roi[3], roi[2]) 97 | return ax 98 | 99 | ax = None 100 | for idx in range(N): 101 | ax = plot_image(idx, 0, idx, shareax=ax) 102 | return fig 103 | -------------------------------------------------------------------------------- /debayer/apps/compare.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | from pathlib import Path 4 | 5 | import cv2 6 | import debayer 7 | import matplotlib.gridspec as gridspec 8 | import matplotlib.pyplot as plt 9 | import torch 10 | 11 | from . import utils 12 | 13 | 14 | @torch.no_grad() 15 | def main(): 16 | logging.basicConfig(level=logging.INFO) 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument("--dev", default="cuda") 19 | parser.add_argument("--half", action="store_true", help="Use 16bit fp precision") 20 | parser.add_argument("--save-zoom", action="store_true", help="Save zoom regions") 21 | parser.add_argument( 22 | "--layout", 23 | type=debayer.Layout, 24 | choices=list(debayer.Layout), 25 | default=debayer.Layout.RGGB, 26 | help="Bayer layout of Bayer input image. Only applicable if --full-color is omitted", # noqa 27 | ) 28 | parser.add_argument( 29 | "--bayer", 30 | action="store_true", 31 | help="If input image is multi-channel, assume encoding is Bayer", 32 | ) 33 | parser.add_argument("image") 34 | args = parser.parse_args() 35 | 36 | prec = torch.float16 if args.half else torch.float32 37 | 38 | methods = { 39 | "Debayer2x2": debayer.Debayer2x2(layout=args.layout).to(args.dev).to(prec), 40 | "Debayer3x3": debayer.Debayer3x3(layout=args.layout).to(args.dev).to(prec), 41 | "DebayerSplit": debayer.DebayerSplit(layout=args.layout).to(args.dev).to(prec), 42 | "Debayer5x5": debayer.Debayer5x5(layout=args.layout).to(args.dev).to(prec), 43 | } 44 | 45 | # Read Bayer image 46 | input_image, b = utils.read_image(args.image, bayer=args.bayer, layout=args.layout) 47 | 48 | # Compute debayer results 49 | # Prepare input with shape Bx1xHxW and 50 | t = (torch.from_numpy(b).to(prec).unsqueeze(0).unsqueeze(0).to(args.dev)) / 255.0 51 | res = { 52 | **{ 53 | "Original": input_image, 54 | "OpenCV": cv2.cvtColor(b, utils.opencv_conversion_code(args.layout)) 55 | / 255.0, 56 | }, 57 | **{ 58 | k: deb(t).squeeze().permute(1, 2, 0).to(torch.float32).cpu().numpy() 59 | for k, deb in methods.items() 60 | }, 61 | } 62 | 63 | nrows = 2 64 | ncols = 3 65 | h, w = b.shape 66 | W, H = plt.figaspect((h * nrows) / (w * ncols)) 67 | 68 | fig = plt.figure(constrained_layout=True, figsize=(W, H)) 69 | spec = gridspec.GridSpec(ncols=ncols, nrows=nrows, figure=fig) 70 | spec.update(wspace=0, hspace=0) 71 | 72 | ax = None 73 | for idx, (key, img) in enumerate(res.items()): 74 | ax = fig.add_subplot(spec[idx], sharey=ax, sharex=ax) 75 | ax.imshow(img, interpolation="nearest") 76 | ax.set_title(key, size=10, y=1.0, pad=-14, color="white") 77 | ax.set_frame_on(False) 78 | ax.tick_params( 79 | bottom=False, 80 | top=False, 81 | left=False, 82 | right=False, 83 | labelleft=False, 84 | labelbottom=False, 85 | ) 86 | 87 | def create_image_mosaic(event_ax): 88 | left, right = event_ax.get_xlim() 89 | top, bottom = event_ax.get_ylim() 90 | return utils.create_mosaic( 91 | list(res.values()), (left, right, bottom, top), list(res.keys()) 92 | ) 93 | 94 | def on_ylims_change(event_ax): 95 | fig = create_image_mosaic(event_ax) 96 | 97 | left, right = event_ax.get_xlim() 98 | bottom, top = event_ax.get_ylim() 99 | 100 | left, right, top, bottom = map(int, [left, right, top, bottom]) 101 | 102 | iname = Path(args.image).with_suffix("").name 103 | path = f"tmp/{iname}-mosaic-l{left}-r{right}-b{bottom}-t{top}.png" 104 | print("Saved to", path) 105 | fig.savefig(path) 106 | plt.close(fig) 107 | 108 | if args.save_zoom: 109 | ax.callbacks.connect("ylim_changed", on_ylims_change) 110 | 111 | plt.show() 112 | 113 | 114 | if __name__ == "__main__": 115 | main() 116 | -------------------------------------------------------------------------------- /debayer/apps/eval.py: -------------------------------------------------------------------------------- 1 | """Evaluates debayer algorithms on single file or folder. 2 | 3 | The images are assumed to be multi-channel RGB images. 4 | 5 | """ 6 | 7 | import argparse 8 | import logging 9 | from collections.abc import Callable 10 | from pathlib import Path 11 | 12 | import cv2 13 | import debayer 14 | import numpy as np 15 | import torch 16 | 17 | from . import utils 18 | from .benchmark import ALL_METHODS 19 | 20 | _logger = logging.getLogger("debayer") 21 | 22 | IMAGE_SUFFIXES = {".png", ".bmp", ".tif"} 23 | LAYOUT = debayer.Layout.RGGB 24 | 25 | 26 | def glob_image_paths(path: Path): 27 | if path.is_file(): 28 | files = [Path(path).resolve()] 29 | elif path.is_dir(): 30 | files = [ 31 | p.resolve() 32 | for p in Path(path).glob("*") 33 | if p.suffix.lower() in IMAGE_SUFFIXES 34 | ] 35 | else: 36 | raise ValueError("Input path is neither file nor folder.") 37 | return sorted(files) 38 | 39 | 40 | def run_opencv(bayer: torch.tensor): 41 | bayer = bayer.squeeze().cpu().to(torch.float32).numpy() * 255.0 42 | bayer = bayer.astype(np.uint8) 43 | rgb = cv2.cvtColor(bayer, cv2.COLOR_BAYER_BG2RGB) 44 | return torch.tensor(rgb).permute(2, 0, 1).to(torch.float32) / 255.0 45 | 46 | 47 | def reconstruct( 48 | path: Path, 49 | methods: dict[str, Callable[[torch.Tensor], torch.Tensor]], 50 | dev: torch.device, 51 | prec: torch.dtype, 52 | ) -> tuple[torch.FloatTensor, torch.BoolTensor]: 53 | """Reconstruct image given by path using given methods and return PSNR values.""" 54 | 55 | orig, bayer = utils.read_image( 56 | path, bayer=False, layout=LAYOUT, loglevel=logging.DEBUG 57 | ) 58 | bpp = orig.dtype.itemsize * 8 59 | max_intensity = (2 ** bpp) - 1 60 | 61 | if orig.ndim != 3 or orig.shape[2] != 3: 62 | raise ValueError(f"Expected RGB image {path}") 63 | 64 | # Reconstruct from Bayer 65 | bayer = ( 66 | torch.tensor(bayer).to(prec).unsqueeze(0).unsqueeze(0).to(dev) 67 | ) / max_intensity 68 | recs = [m(bayer).squeeze().cpu().to(torch.float32) for m in methods.values()] 69 | recs = torch.stack(recs, 0) # (B,C,H,W) 70 | 71 | # Original image 72 | orig = torch.tensor(orig).permute(2, 0, 1).to(torch.float32) / max_intensity 73 | orig = orig.unsqueeze(0).repeat(recs.shape[0], 1, 1, 1) 74 | 75 | # Compare 76 | psnr, eq_mask = debayer.metrics.peak_signal_noise_ratio(orig, recs, datarange=1.0) 77 | 78 | return psnr, eq_mask 79 | 80 | 81 | @torch.no_grad() 82 | def main(): 83 | logging.basicConfig(level=logging.INFO) 84 | parser = argparse.ArgumentParser() 85 | parser.add_argument( 86 | "--methods", 87 | default=["Debayer3x3", "Debayer5x5", "OpenCV"], 88 | nargs="+", 89 | help="Which methods to run. List of methods or `all`.", 90 | choices=ALL_METHODS + ["all"], 91 | ) 92 | parser.add_argument("--half", action="store_true", help="Use 16bit fp precision") 93 | parser.add_argument("--dev", default="cuda") 94 | parser.add_argument("path", type=Path, help="Image file or folder") 95 | args = parser.parse_args() 96 | 97 | # Glob files 98 | image_paths = glob_image_paths(args.path) 99 | if len(image_paths) == 0: 100 | _logger.warning("No image files found") 101 | return 102 | 103 | # Setup precision and algorithms 104 | prec = torch.float16 if args.half else torch.float32 105 | if "all" in args.methods: 106 | args.methods = ALL_METHODS 107 | methods = { 108 | "Debayer2x2": debayer.Debayer2x2(layout=LAYOUT).to(args.dev).to(prec), 109 | "Debayer3x3": debayer.Debayer3x3(layout=LAYOUT).to(args.dev).to(prec), 110 | "DebayerSplit": debayer.DebayerSplit(layout=LAYOUT).to(args.dev).to(prec), 111 | "Debayer5x5": debayer.Debayer5x5(layout=LAYOUT).to(args.dev).to(prec), 112 | "OpenCV": run_opencv, 113 | } 114 | methods = {k: v for k, v in methods.items() if k in args.methods} 115 | _logger.info(f"Enabled methods {list(methods.keys())}") 116 | 117 | results = [] 118 | for path in image_paths: 119 | _logger.info(f"Processing {path}") 120 | psnrs, eqmasks = reconstruct(path, methods, args.dev, prec) 121 | for method, psnr, eqmask in zip(methods.keys(), psnrs, eqmasks): 122 | results.append( 123 | { 124 | "Path": path, 125 | "Database": args.path.name, 126 | "Method": method, 127 | "R (dB)": psnr[0].item(), 128 | "G (dB)": psnr[1].item(), 129 | "B (dB)": psnr[2].item(), 130 | "PSNR (dB)": psnr.mean().item(), 131 | "Equal": eqmask.any().item(), 132 | } 133 | ) 134 | import pandas as pd 135 | 136 | df = pd.DataFrame(results) 137 | print() 138 | print( 139 | df.groupby([df.Database, df.Method])[ 140 | ["Database", "Method", "R (dB)", "G (dB)", "B (dB)", "PSNR (dB)"] 141 | ] 142 | .mean() 143 | .reset_index() 144 | .to_markdown(headers="keys", index=False, floatfmt=".2f", tablefmt="github") 145 | ) 146 | 147 | 148 | if __name__ == "__main__": 149 | main() 150 | -------------------------------------------------------------------------------- /debayer/apps/benchmark.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import argparse 3 | import time 4 | 5 | import cpuinfo 6 | import torch 7 | from itertools import product 8 | 9 | from . import utils 10 | 11 | try: 12 | import cv2 13 | 14 | OPENCV_AVAILABLE = True 15 | except ImportError: 16 | print("Skipping OpenCV, not installed.") 17 | OPENCV_AVAILABLE = False 18 | 19 | import debayer 20 | 21 | 22 | def run_pytorch(deb, t, dev, kwargs: dict): 23 | time_upload = kwargs.get("time_upload", False) 24 | B = kwargs.get("batch", 10) 25 | runs = kwargs.get("runs", 100) 26 | prec = kwargs.get("prec", torch.float32) 27 | 28 | t = t.repeat(B, 1, 1, 1).to(prec).contiguous() 29 | t = t.pin_memory() if time_upload else t.to(dev) 30 | 31 | def run_once(): 32 | x = t.to(dev, non_blocking=True) if time_upload else t 33 | rgb = deb(x) # noqa 34 | 35 | # Warmup 36 | run_once() 37 | run_once() 38 | run_once() 39 | 40 | if dev != "cpu": 41 | torch.cuda.synchronize() 42 | start = torch.cuda.Event(enable_timing=True) 43 | end = torch.cuda.Event(enable_timing=True) 44 | 45 | start.record() 46 | N = runs 47 | for _ in range(N): 48 | run_once() 49 | end.record() 50 | torch.cuda.synchronize() 51 | 52 | return start.elapsed_time(end) / (runs * B) 53 | else: 54 | t0 = time.perf_counter() 55 | N = runs 56 | for _ in range(N): 57 | run_once() 58 | return (time.perf_counter() - t0) / (runs * B) * 1e3 59 | 60 | 61 | def run_opencv(b, **kwargs): 62 | # see https://www.learnopencv.com/opencv-transparent-api/ 63 | num_threads = kwargs.get("opencv-threads", None) 64 | if num_threads is not None: 65 | cv2.setNumThreads(num_threads) 66 | time_upload = kwargs.get("time_upload", False) 67 | transparent_api = kwargs.get("transparent_api", False) 68 | runs = kwargs.get("runs", 100) 69 | layout = kwargs.get("layout", debayer.Layout.RGGB) 70 | B = kwargs.get("batch_size", 10) 71 | 72 | b = cv2.UMat(b) if (transparent_api and not time_upload) else b 73 | 74 | def run_once(): 75 | x = cv2.UMat(b) if (transparent_api and time_upload) else b 76 | y = cv2.cvtColor(x, utils.opencv_conversion_code(layout)) 77 | return y 78 | 79 | run_once() 80 | run_once() 81 | y = run_once() 82 | if transparent_api: 83 | z = y.get() 84 | 85 | N = runs * B 86 | start = time.perf_counter() 87 | for _ in range(N): 88 | y = run_once() 89 | if transparent_api: 90 | z = y.get() # noqa 91 | return (time.perf_counter() - start) / N * 1e3 92 | 93 | 94 | def fmt_line(method, devname, elapsed, **modeargs): 95 | modeargs.pop("batch_size", None) 96 | modeargs.pop("runs", None) 97 | modeargs.pop("layout", None) 98 | modeargs.pop("bayer", None) 99 | modeargs.pop("image", None) 100 | modeargs.pop("methods", None) 101 | modeargs.pop("dev", None) 102 | mode = ",".join([f"{k}={v}" for k, v in modeargs.items()]) 103 | return f"| {method} | {devname} | {elapsed:5.3f} | {mode} |" 104 | 105 | 106 | @torch.no_grad() 107 | def bench_debayer(b, args): 108 | if args.dev != "cpu": 109 | devname = torch.cuda.get_device_name(args.dev) 110 | else: 111 | devname = cpuinfo.get_cpu_info()["brand_raw"] 112 | mode = vars(args) 113 | 114 | def run_all(dev, mode): 115 | t = (torch.tensor(b).clone().unsqueeze(0).unsqueeze(0)) / 255.0 116 | prec = mode["prec"] 117 | mods = { 118 | "Debayer2x2": debayer.Debayer2x2(layout=mode["layout"]).to(prec).to(dev), 119 | "Debayer3x3": debayer.Debayer3x3(layout=mode["layout"]).to(prec).to(dev), 120 | "Debayer5x5": debayer.Debayer5x5(layout=mode["layout"]).to(prec).to(dev), 121 | "DebayerSplit": debayer.DebayerSplit(layout=mode["layout"]) 122 | .to(prec) 123 | .to(dev), 124 | } 125 | mods = {k: v for k, v in mods.items() if k in args.methods} 126 | if mode["torchscript"]: 127 | mods = { 128 | k: torch.jit.script( 129 | torch.jit.trace(v, torch.rand(1, 1, 128, 128).to(dev).to(prec)) 130 | ) 131 | for k, v in mods.items() 132 | } 133 | 134 | for name, mod in mods.items(): 135 | e = run_pytorch(mod, t, dev, mode) 136 | print(fmt_line(name, devname, e, **mode)) 137 | 138 | for prec, script in product([torch.float32, torch.float16], [True, False]): 139 | run_all(args.dev, mode={**mode, **{"prec": prec, "torchscript": script}}) 140 | 141 | 142 | def bench_opencv(b, args): 143 | if "OpenCV" not in args.methods: 144 | return 145 | devname = cpuinfo.get_cpu_info()["brand_raw"] 146 | 147 | for threads, transparent in product([4, 12], [False, True]): 148 | mode = {**vars(args), "opencv-threads": threads, "transparent-api": transparent} 149 | e = run_opencv(b, **mode) 150 | print(fmt_line(f"OpenCV {cv2.__version__}", devname, e, **mode)) 151 | 152 | 153 | ALL_METHODS = ["Debayer2x2", "Debayer3x3", "Debayer5x5", "DebayerSplit", "OpenCV"] 154 | 155 | 156 | def main(): 157 | logging.basicConfig(level=logging.INFO) 158 | parser = argparse.ArgumentParser() 159 | parser.add_argument("--dev", default="cuda") 160 | parser.add_argument("--batch", default=10, type=int) 161 | parser.add_argument("--time-upload", action="store_true") 162 | parser.add_argument("--runs", type=int, default=100, help="Number runs") 163 | parser.add_argument( 164 | "--methods", 165 | default=["Debayer3x3", "Debayer5x5", "OpenCV"], 166 | nargs="+", 167 | help="Which methods to run. List of methods or `all`.", 168 | choices=ALL_METHODS + ["all"], 169 | ) 170 | parser.add_argument( 171 | "--layout", 172 | type=debayer.Layout, 173 | choices=list(debayer.Layout), 174 | default=debayer.Layout.RGGB, 175 | help="Bayer layout of Bayer input image. Only applicable if --full-color is omitted", # noqa 176 | ) 177 | parser.add_argument( 178 | "--bayer", 179 | action="store_true", 180 | help="If input image is multi-channel, assume encoding is Bayer", 181 | ) 182 | parser.add_argument("image") 183 | args = parser.parse_args() 184 | if "all" in args.methods: 185 | args.methods = ALL_METHODS 186 | 187 | input_image, b = utils.read_image(args.image, bayer=args.bayer, layout=args.layout) 188 | 189 | print(f"torch: v{torch.__version__}") 190 | print(f"pytorch-debayer: v{debayer.__version__}") 191 | 192 | print() 193 | mpix = b.size * 1e-6 194 | print(f"Method | Device | Elapsed [msec / {mpix:.1f}mpix] | Mode |") 195 | print("|:----:|:------:|:-------:|:----:|") 196 | 197 | bench_debayer(b, args) 198 | if OPENCV_AVAILABLE: 199 | bench_opencv(b, args) 200 | 201 | 202 | if __name__ == "__main__": 203 | main() 204 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/cheind/pytorch-debayer/actions/workflows/python-package.yml/badge.svg)](https://github.com/cheind/pytorch-debayer/actions/workflows/python-package.yml) 2 | 3 | # pytorch-debayer 4 | 5 | Provides batch GPU demosaicing of images captured by Bayer color filter array (CFA) cameras. This implementation relies on pure PyTorch functionality and thus avoids any extra build steps. This library is most useful when downstream image processing happens with PyTorch models. Additionally, uploading of Bayer images (instead of RGB) significantly reduces the occupied bandwidth. 6 | 7 | ## Features 8 | - **Methods** Currently, the following methods are provided 9 | - `debayer.Debayer2x2` uses 2x2 convolutions. Trades speed for color accuracy. 10 | - `debayer.Debayer3x3` uses 3x3 convolutions. Slower but reconstruction results comparable with `OpenCV.cvtColor`. 11 | - `debayer.Debayer5x5` uses 5x5 convolutions based on Malver-He-Cutler algorithm. Slower but sharper than `OpenCV.cvtColor`. Should be your default. 12 | - `debayer.DebayerSplit` faster than `debayer.Debayer3x3` but decreased image quality. 13 | - **Precision** Each method supports `float32` or `float16` precision. 14 | 15 | ## Usage 16 | Usage is straight forward 17 | 18 | ```python 19 | import torch 20 | from debayer import Debayer5x5 21 | 22 | f = Debayer5x5().cuda() 23 | 24 | bayer = ... # a Bx1xHxW, [0..1], torch.float32 RGGB-Bayer tensor 25 | with torch.no_grad(): 26 | rgb = f(bayer) # a Bx3xHxW, torch.float32 tensor of RGB images 27 | ``` 28 | 29 | see [this example](debayer/apps/example.py) for elaborate code. 30 | 31 | ## Install 32 | Library, apps and development tools 33 | ``` 34 | pip install git+https://github.com/cheind/pytorch-debayer#egg=pytorch-debayer[full] 35 | ``` 36 | 37 | Just the library core requirements 38 | ``` 39 | pip install git+https://github.com/cheind/pytorch-debayer 40 | ``` 41 | 42 | ## Bayer Layouts 43 | Bayer filter arrays may come in different layouts. **pytorch-debayer** distinguishes these layouts by looking at the upper-left 2x2 pixel block. For example 44 | ``` 45 | RGrg... 46 | GBgb... 47 | rgrg... 48 | ``` 49 | defines the `Layout.RGGB` which is also the default. In total four layouts are supported 50 | ```python 51 | from debayer import Layout 52 | 53 | Layout.RGGB 54 | Layout.GRBG 55 | Layout.GBRG 56 | Layout.BGGR 57 | ``` 58 | 59 | and you can set the layout as follows 60 | 61 | ```python 62 | from debayer import Debayer5x5, Layout 63 | 64 | f = Debayer5x5(layout=Layout.BGGR).cuda() 65 | ``` 66 | 67 | ## Evaluation 68 | 69 | ### PSNR values 70 | The PSNR (Peak-Signal-Noise-Ratio) values (dB, higher is better) for each channel (R, G, B) and PSNR of the whole image (RGB) across 2 Datasets (Kodak, McMaster) and for each algorithm. See [Metrics.md](./Metrics.md) for additional details. 71 | 72 | | Database | Method | R (dB)| G (dB)| B (dB)| PSNR (dB)| 73 | |------------|--------------|-------|-------|-------|--------| 74 | | Kodak | Debayer2x2 | 26.64 | 28.18 | 26.98 | 27.27 | 75 | | | Debayer3x3 | 28.18 | 32.66 | 28.86 | 29.90 | 76 | | | Debayer5x5 | 33.84 | 38.05 | 33.53 | 35.14 | 77 | | | DebayerSplit | 26.64 | 32.66 | 26.98 | 28.76 | 78 | | | OpenCV | 28.15 | 31.25 | 28.62 | 29.34 | 79 | | McMaster | Debayer2x2 | 28.47 | 30.32 | 28.63 | 29.14 | 80 | | | Debayer3x3 | 31.68 | 35.40 | 31.25 | 32.78 | 81 | | | Debayer5x5 | 34.04 | 37.62 | 33.02 | 34.89 | 82 | | | DebayerSplit | 28.47 | 35.40 | 28.63 | 30.83 | 83 | | | OpenCV | 31.64 | 35.22 | 31.22 | 32.69 | 84 | 85 | 86 | ### Runtimes 87 | Performance comparison on a 5 megapixel [test image](etc/test.bmp) using a batch size of 10. 88 | Timings are in milliseconds per given megapixels. See [Benchmarks.md](./Benchmarks.md) for additional details. 89 | 90 | Method | Device | Elapsed [msec/5.1mpix] | Mode | 91 | |:----:|:------:|:-------:|:----:| 92 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.617 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 93 | | Debayer3x3 | GeForce GTX 1080 Ti | 3.298 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 94 | | Debayer5x5 | GeForce GTX 1080 Ti | 5.842 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 95 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.563 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 96 | | Debayer3x3 | GeForce GTX 1080 Ti | 2.927 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 97 | | Debayer5x5 | GeForce GTX 1080 Ti | 4.044 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 98 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.231 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 99 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 1.052 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 100 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.610 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 101 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.174 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 102 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 0.854 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 103 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.589 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 104 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 2.205 | batch=10,time_upload=False,opencv-threads=4,transparent-api=False | 105 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 2.206 | batch=10,time_upload=False,opencv-threads=4,transparent-api=True | 106 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 1.937 | batch=10,time_upload=False,opencv-threads=12,transparent-api=False | 107 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 1.925 | batch=10,time_upload=False,opencv-threads=12,transparent-api=True | 108 | 109 | 110 | ### Subjective Results 111 | 112 | Here are some subjective image demosaicing results using the following [test image](etc/test.bmp) image. 113 |
114 | 115 |
116 | 117 | The following highlights algorithmic differences on various smaller regions for improved pixel visibility. From left to right 118 | ``` 119 | OpenCV, Debayer2x2, Debayer3x3, DebayerSplit, Debayer5x5 120 | ``` 121 | 122 | Click images to enlarge. 123 | 124 |
125 | 126 |
127 | 128 |
129 | 130 |
131 | 132 |
133 | 134 |
135 | 136 |
137 | 138 |
139 | 140 | Created using 141 | ``` 142 | python -m debayer.apps.compare etc\test.bmp 143 | # Then select a region and check `tmp`/ 144 | ``` 145 | 146 | ## Limitations 147 | 148 | Currently **pytorch-debayer** requires 149 | - the image to have an even number of rows and columns 150 | - `debayer.DebayerSplit` requires a Bayer filter layout of `Layout.RGGB`, all others support varying layouts (since v1.3.0). 151 | 152 | ## References 153 | The following reference are mostly for comparison metrics and not algorithms. See the individual module documentation for algorithmic references. 154 | 155 | - Wang, Shuyu, et al. "A Compact High-Quality Image Demosaicking Neural Network for Edge-Computing Devices." Sensors 21.9 (2021): 3265. 156 | 157 | - Losson, Olivier, Ludovic Macaire, and Yanqin Yang. "Comparison of color demosaicing methods." Advances in Imaging and electron Physics. Vol. 162. Elsevier, 2010. 173-265. 158 | -------------------------------------------------------------------------------- /debayer/modules.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn 3 | import torch.nn.functional 4 | 5 | from .layouts import Layout 6 | 7 | 8 | class Debayer3x3(torch.nn.Module): 9 | """Demosaicing of Bayer images using 3x3 convolutions. 10 | 11 | Compared to Debayer2x2 this method does not use upsampling. 12 | Instead, we identify five 3x3 interpolation kernels that 13 | are sufficient to reconstruct every color channel at every 14 | pixel location. 15 | 16 | We convolve the image with these 5 kernels using stride=1 17 | and a one pixel reflection padding. Finally, we gather 18 | the correct channel values for each pixel location. Todo so, 19 | we recognize that the Bayer pattern repeats horizontally and 20 | vertically every 2 pixels. Therefore, we define the correct 21 | index lookups for a 2x2 grid cell and then repeat to image 22 | dimensions. 23 | """ 24 | 25 | def __init__(self, layout: Layout = Layout.RGGB): 26 | super(Debayer3x3, self).__init__() 27 | self.layout = layout 28 | # fmt: off 29 | self.kernels = torch.nn.Parameter( 30 | torch.tensor( 31 | [ 32 | [0, 0.25, 0], 33 | [0.25, 0, 0.25], 34 | [0, 0.25, 0], 35 | 36 | [0.25, 0, 0.25], 37 | [0, 0, 0], 38 | [0.25, 0, 0.25], 39 | 40 | [0, 0, 0], 41 | [0.5, 0, 0.5], 42 | [0, 0, 0], 43 | 44 | [0, 0.5, 0], 45 | [0, 0, 0], 46 | [0, 0.5, 0], 47 | ] 48 | ).view(4, 1, 3, 3), 49 | requires_grad=False, 50 | ) 51 | # fmt: on 52 | 53 | self.index = torch.nn.Parameter( 54 | self._index_from_layout(layout), 55 | requires_grad=False, 56 | ) 57 | 58 | def forward(self, x): 59 | """Debayer image. 60 | 61 | Parameters 62 | ---------- 63 | x : Bx1xHxW tensor 64 | Images to debayer 65 | 66 | Returns 67 | ------- 68 | rgb : Bx3xHxW tensor 69 | Color images in RGB channel order. 70 | """ 71 | B, C, H, W = x.shape 72 | 73 | xpad = torch.nn.functional.pad(x, (1, 1, 1, 1), mode="reflect") 74 | c = torch.nn.functional.conv2d(xpad, self.kernels, stride=1) 75 | c = torch.cat((c, x), 1) # Concat with input to give identity kernel Bx5xHxW 76 | 77 | rgb = torch.gather( 78 | c, 79 | 1, 80 | self.index.repeat( 81 | 1, 82 | 1, 83 | torch.div(H, 2, rounding_mode="floor"), 84 | torch.div(W, 2, rounding_mode="floor"), 85 | ).expand( 86 | B, -1, -1, -1 87 | ), # expand in batch is faster than repeat 88 | ) 89 | return rgb 90 | 91 | def _index_from_layout(self, layout: Layout) -> torch.Tensor: 92 | """Returns a 1x3x2x2 index tensor for each color RGB in a 2x2 bayer tile. 93 | 94 | Note, the index corresponding to the identity kernel is 4, which will be 95 | correct after concatenating the convolved output with the input image. 96 | """ 97 | # ... 98 | # ... b g b g ... 99 | # ... g R G r ... 100 | # ... b G B g ... 101 | # ... g r g r ... 102 | # ... 103 | # fmt: off 104 | rggb = torch.tensor( 105 | [ 106 | # dest channel r 107 | [4, 2], # pixel is R,G1 108 | [3, 1], # pixel is G2,B 109 | # dest channel g 110 | [0, 4], # pixel is R,G1 111 | [4, 0], # pixel is G2,B 112 | # dest channel b 113 | [1, 3], # pixel is R,G1 114 | [2, 4], # pixel is G2,B 115 | ] 116 | ).view(1, 3, 2, 2) 117 | # fmt: on 118 | return { 119 | Layout.RGGB: rggb, 120 | Layout.GRBG: torch.roll(rggb, 1, -1), 121 | Layout.GBRG: torch.roll(rggb, 1, -2), 122 | Layout.BGGR: torch.roll(rggb, (1, 1), (-1, -2)), 123 | }.get(layout) 124 | 125 | 126 | class Debayer2x2(torch.nn.Module): 127 | """Fast demosaicing of Bayer images using 2x2 convolutions. 128 | 129 | This method uses 3 kernels of size 2x2 and stride 2. Each kernel 130 | corresponds to a single color RGB. For R and B the corresponding 131 | value from each 2x2 Bayer block is taken according to the layout. 132 | For G, G1 and G2 are averaged. The resulting image has half width/ 133 | height and is upsampled by a factor of 2. 134 | """ 135 | 136 | def __init__(self, layout: Layout = Layout.RGGB): 137 | super(Debayer2x2, self).__init__() 138 | self.layout = layout 139 | 140 | self.kernels = torch.nn.Parameter( 141 | self._kernels_from_layout(layout), 142 | requires_grad=False, 143 | ) 144 | 145 | def forward(self, x): 146 | """Debayer image. 147 | 148 | Parameters 149 | ---------- 150 | x : Bx1xHxW tensor 151 | Images to debayer 152 | 153 | Returns 154 | ------- 155 | rgb : Bx3xHxW tensor 156 | Color images in RGB channel order. 157 | """ 158 | x = torch.nn.functional.conv2d(x, self.kernels, stride=2) 159 | 160 | x = torch.nn.functional.interpolate( 161 | x, scale_factor=2, mode="bilinear", align_corners=False 162 | ) 163 | return x 164 | 165 | def _kernels_from_layout(self, layout: Layout) -> torch.Tensor: 166 | v = torch.tensor(layout.value).reshape(2, 2) 167 | r = torch.zeros(2, 2) 168 | r[v == 0] = 1.0 169 | 170 | g = torch.zeros(2, 2) 171 | g[v == 1] = 0.5 172 | 173 | b = torch.zeros(2, 2) 174 | b[v == 2] = 1.0 175 | 176 | k = torch.stack((r, g, b), 0).unsqueeze(1) # 3x1x2x2 177 | return k 178 | 179 | 180 | class DebayerSplit(torch.nn.Module): 181 | """Demosaicing of Bayer images using 3x3 green convolution and red,blue upsampling. 182 | Requires Bayer layout `Layout.RGGB`. 183 | """ 184 | 185 | def __init__(self, layout: Layout = Layout.RGGB): 186 | super().__init__() 187 | if layout != Layout.RGGB: 188 | raise NotImplementedError("DebayerSplit only implemented for RGGB layout.") 189 | self.layout = layout 190 | 191 | self.pad = torch.nn.ReflectionPad2d(1) 192 | self.kernel = torch.nn.Parameter( 193 | torch.tensor([[0, 1, 0], [1, 0, 1], [0, 1, 0]])[None, None] * 0.25 194 | ) 195 | 196 | def forward(self, x): 197 | """Debayer image. 198 | 199 | Parameters 200 | ---------- 201 | x : Bx1xHxW tensor 202 | Images to debayer 203 | 204 | Returns 205 | ------- 206 | rgb : Bx3xHxW tensor 207 | Color images in RGB channel order. 208 | """ 209 | B, _, H, W = x.shape 210 | red = x[:, :, ::2, ::2] 211 | blue = x[:, :, 1::2, 1::2] 212 | 213 | green = torch.nn.functional.conv2d(self.pad(x), self.kernel) 214 | green[:, :, ::2, 1::2] = x[:, :, ::2, 1::2] 215 | green[:, :, 1::2, ::2] = x[:, :, 1::2, ::2] 216 | 217 | return torch.cat( 218 | ( 219 | torch.nn.functional.interpolate( 220 | red, size=(H, W), mode="bilinear", align_corners=False 221 | ), 222 | green, 223 | torch.nn.functional.interpolate( 224 | blue, size=(H, W), mode="bilinear", align_corners=False 225 | ), 226 | ), 227 | dim=1, 228 | ) 229 | 230 | 231 | class Debayer5x5(torch.nn.Module): 232 | """Demosaicing of Bayer images using Malver-He-Cutler algorithm. 233 | 234 | Requires BG-Bayer color filter array layout. That is, 235 | the image[1,1]='B', image[1,2]='G'. This corresponds 236 | to OpenCV naming conventions. 237 | 238 | Compared to Debayer2x2 this method does not use upsampling. 239 | Compared to Debayer3x3 the algorithm gives sharper edges and 240 | less chromatic effects. 241 | 242 | ## References 243 | Malvar, Henrique S., Li-wei He, and Ross Cutler. 244 | "High-quality linear interpolation for demosaicing of Bayer-patterned 245 | color images." 2004 246 | """ 247 | 248 | def __init__(self, layout: Layout = Layout.RGGB): 249 | super(Debayer5x5, self).__init__() 250 | self.layout = layout 251 | # fmt: off 252 | self.kernels = torch.nn.Parameter( 253 | torch.tensor( 254 | [ 255 | # G at R,B locations 256 | # scaled by 16 257 | [ 0, 0, -2, 0, 0], # noqa 258 | [ 0, 0, 4, 0, 0], # noqa 259 | [-2, 4, 8, 4, -2], # noqa 260 | [ 0, 0, 4, 0, 0], # noqa 261 | [ 0, 0, -2, 0, 0], # noqa 262 | 263 | # R,B at G in R rows 264 | # scaled by 16 265 | [ 0, 0, 1, 0, 0], # noqa 266 | [ 0, -2, 0, -2, 0], # noqa 267 | [-2, 8, 10, 8, -2], # noqa 268 | [ 0, -2, 0, -2, 0], # noqa 269 | [ 0, 0, 1, 0, 0], # noqa 270 | 271 | # R,B at G in B rows 272 | # scaled by 16 273 | [ 0, 0, -2, 0, 0], # noqa 274 | [ 0, -2, 8, -2, 0], # noqa 275 | [ 1, 0, 10, 0, 1], # noqa 276 | [ 0, -2, 8, -2, 0], # noqa 277 | [ 0, 0, -2, 0, 0], # noqa 278 | 279 | # R at B and B at R 280 | # scaled by 16 281 | [ 0, 0, -3, 0, 0], # noqa 282 | [ 0, 4, 0, 4, 0], # noqa 283 | [-3, 0, 12, 0, -3], # noqa 284 | [ 0, 4, 0, 4, 0], # noqa 285 | [ 0, 0, -3, 0, 0], # noqa 286 | 287 | # R at R, B at B, G at G 288 | # identity kernel not shown 289 | ] 290 | ).view(4, 1, 5, 5).float() / 16.0, 291 | requires_grad=False, 292 | ) 293 | # fmt: on 294 | 295 | self.index = torch.nn.Parameter( 296 | # Below, note that index 4 corresponds to identity kernel 297 | self._index_from_layout(layout), 298 | requires_grad=False, 299 | ) 300 | 301 | def forward(self, x): 302 | """Debayer image. 303 | 304 | Parameters 305 | ---------- 306 | x : Bx1xHxW tensor 307 | Images to debayer 308 | 309 | Returns 310 | ------- 311 | rgb : Bx3xHxW tensor 312 | Color images in RGB channel order. 313 | """ 314 | B, C, H, W = x.shape 315 | 316 | xpad = torch.nn.functional.pad(x, (2, 2, 2, 2), mode="reflect") 317 | planes = torch.nn.functional.conv2d(xpad, self.kernels, stride=1) 318 | planes = torch.cat( 319 | (planes, x), 1 320 | ) # Concat with input to give identity kernel Bx5xHxW 321 | rgb = torch.gather( 322 | planes, 323 | 1, 324 | self.index.repeat( 325 | 1, 326 | 1, 327 | torch.div(H, 2, rounding_mode="floor"), 328 | torch.div(W, 2, rounding_mode="floor"), 329 | ).expand( 330 | B, -1, -1, -1 331 | ), # expand for singleton batch dimension is faster 332 | ) 333 | return torch.clamp(rgb, 0, 1) 334 | 335 | def _index_from_layout(self, layout: Layout) -> torch.Tensor: 336 | """Returns a 1x3x2x2 index tensor for each color RGB in a 2x2 bayer tile. 337 | 338 | Note, the index corresponding to the identity kernel is 4, which will be 339 | correct after concatenating the convolved output with the input image. 340 | """ 341 | # ... 342 | # ... b g b g ... 343 | # ... g R G r ... 344 | # ... b G B g ... 345 | # ... g r g r ... 346 | # ... 347 | # fmt: off 348 | rggb = torch.tensor( 349 | [ 350 | # dest channel r 351 | [4, 1], # pixel is R,G1 352 | [2, 3], # pixel is G2,B 353 | # dest channel g 354 | [0, 4], # pixel is R,G1 355 | [4, 0], # pixel is G2,B 356 | # dest channel b 357 | [3, 2], # pixel is R,G1 358 | [1, 4], # pixel is G2,B 359 | ] 360 | ).view(1, 3, 2, 2) 361 | # fmt: on 362 | return { 363 | Layout.RGGB: rggb, 364 | Layout.GRBG: torch.roll(rggb, 1, -1), 365 | Layout.GBRG: torch.roll(rggb, 1, -2), 366 | Layout.BGGR: torch.roll(rggb, (1, 1), (-1, -2)), 367 | }.get(layout) 368 | -------------------------------------------------------------------------------- /Benchmarks.md: -------------------------------------------------------------------------------- 1 | # Additional Benchmark Results 2 | 3 | Performance comparison on a 5 megapixel [test image](etc/test.bmp). Command is 4 | ``` 5 | python -m debayer.apps.benchmark etc/test.bmp --methods all 6 | ``` 7 | Note, in case the latest version below does not match the current version of pytorch-debayer, performance statistics to remain unchanged. 8 | 9 | ## Version 1.4.1 10 | 11 | ### Machine 1 12 | torch: v1.10.0+cu113 13 | pytorch-debayer: v1.4.1 14 | 15 | Method | Device | Elapsed [msec / 5.1mpix] | Mode | 16 | |:----:|:------:|:-------:|:----:| 17 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.619 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 18 | | Debayer3x3 | GeForce GTX 1080 Ti | 3.296 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 19 | | Debayer5x5 | GeForce GTX 1080 Ti | 5.837 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 20 | | DebayerSplit | GeForce GTX 1080 Ti | 2.631 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 21 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.617 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 22 | | Debayer3x3 | GeForce GTX 1080 Ti | 3.298 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 23 | | Debayer5x5 | GeForce GTX 1080 Ti | 5.842 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 24 | | DebayerSplit | GeForce GTX 1080 Ti | 2.632 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 25 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.561 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 26 | | Debayer3x3 | GeForce GTX 1080 Ti | 2.919 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 27 | | Debayer5x5 | GeForce GTX 1080 Ti | 4.045 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 28 | | DebayerSplit | GeForce GTX 1080 Ti | 1.151 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 29 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.563 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 30 | | Debayer3x3 | GeForce GTX 1080 Ti | 2.927 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 31 | | Debayer5x5 | GeForce GTX 1080 Ti | 4.044 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 32 | | DebayerSplit | GeForce GTX 1080 Ti | 1.151 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 33 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 2.205 | batch=10,time_upload=False,opencv-threads=4,transparent-api=False | 34 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 2.206 | batch=10,time_upload=False,opencv-threads=4,transparent-api=True | 35 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 1.937 | batch=10,time_upload=False,opencv-threads=12,transparent-api=False | 36 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 1.925 | batch=10,time_upload=False,opencv-threads=12,transparent-api=True | 37 | 38 | ### Machine 2 39 | torch: v1.11.0+cu113 40 | pytorch-debayer: v1.4.1 41 | 42 | Method | Device | Elapsed [msec / 5.1mpix] | Mode | 43 | |:----:|:------:|:-------:|:----:| 44 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.234 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 45 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 1.048 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 46 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.610 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 47 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 12.049 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 48 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.231 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 49 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 1.052 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 50 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.610 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 51 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 11.969 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 52 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.174 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 53 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 0.854 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 54 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.590 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 55 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 0.371 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 56 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.174 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 57 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 0.854 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 58 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.589 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 59 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 0.371 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 60 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 0.878 | batch=10,time_upload=False,opencv-threads=4,transparent-api=False | 61 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 0.842 | batch=10,time_upload=False,opencv-threads=4,transparent-api=True | 62 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 0.819 | batch=10,time_upload=False,opencv-threads=12,transparent-api=False | 63 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 0.821 | batch=10,time_upload=False,opencv-threads=12,transparent-api=True | 64 | 65 | 66 | ## Version 1.3.0 67 | 68 | ### Machine 1 69 | torch: v1.10.0+cu113 70 | pytorch-debayer: v1.3.0 71 | 72 | Method | Device | Elapsed [msec / 5.1mpix] | Mode | 73 | |:----:|:------:|:-------:|:----:| 74 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.633 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 75 | | Debayer3x3 | GeForce GTX 1080 Ti | 4.043 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 76 | | Debayer5x5 | GeForce GTX 1080 Ti | 6.719 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 77 | | DebayerSplit | GeForce GTX 1080 Ti | 2.689 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 78 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.631 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 79 | | Debayer3x3 | GeForce GTX 1080 Ti | 3.874 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 80 | | Debayer5x5 | GeForce GTX 1080 Ti | 6.487 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 81 | | DebayerSplit | GeForce GTX 1080 Ti | 2.697 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 82 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.575 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 83 | | Debayer3x3 | GeForce GTX 1080 Ti | 3.498 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 84 | | Debayer5x5 | GeForce GTX 1080 Ti | 4.640 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 85 | | DebayerSplit | GeForce GTX 1080 Ti | 1.173 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 86 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.574 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 87 | | Debayer3x3 | GeForce GTX 1080 Ti | 3.498 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 88 | | Debayer5x5 | GeForce GTX 1080 Ti | 4.640 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 89 | | DebayerSplit | GeForce GTX 1080 Ti | 1.173 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 90 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 2.221 | batch=10,time_upload=False,opencv-threads=4,transparent-api=False | 91 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 2.202 | batch=10,time_upload=False,opencv-threads=4,transparent-api=True | 92 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 1.968 | batch=10,time_upload=False,opencv-threads=12,transparent-api=False | 93 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 1.956 | batch=10,time_upload=False,opencv-threads=12,transparent-api=True | 94 | 95 | ### Machine 2 96 | torch: v1.11.0+cu113 97 | pytorch-debayer: v1.3.0 98 | 99 | Method | Device | Elapsed [msec / 5.1mpix] | Mode | 100 | |:----:|:------:|:-------:|:----:| 101 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.231 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 102 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 1.173 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 103 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.735 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 104 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 12.023 | batch=10,time_upload=False,prec=torch.float32,torchscript=True | 105 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.231 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 106 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 1.173 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 107 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.733 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 108 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 11.966 | batch=10,time_upload=False,prec=torch.float32,torchscript=False | 109 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.173 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 110 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 0.981 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 111 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.700 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 112 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 0.373 | batch=10,time_upload=False,prec=torch.float16,torchscript=True | 113 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.175 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 114 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 0.982 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 115 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.701 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 116 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 0.373 | batch=10,time_upload=False,prec=torch.float16,torchscript=False | 117 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 0.708 | batch=10,time_upload=False,opencv-threads=4,transparent-api=False | 118 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 0.751 | batch=10,time_upload=False,opencv-threads=4,transparent-api=True | 119 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 0.816 | batch=10,time_upload=False,opencv-threads=12,transparent-api=False | 120 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 0.728 | batch=10,time_upload=False,opencv-threads=12,transparent-api=True | 121 | 122 | 123 | ## Version 1.1.0 124 | 125 | ### Machine 1 126 | torch: v1.10.0+cu113 127 | pytorch-debayer: v1.1.0 128 | 129 | Method | Device | Elapsed [msec/image] | Mode | 130 | |:----:|:------:|:-------:|:----:| 131 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.653 | prec=torch.float32,time_upload=False | 132 | | Debayer3x3 | GeForce GTX 1080 Ti | 3.056 | prec=torch.float32,time_upload=False | 133 | | Debayer5x5 | GeForce GTX 1080 Ti | 6.343 | prec=torch.float32,time_upload=False | 134 | | DebayerSplit | GeForce GTX 1080 Ti | 2.635 | prec=torch.float32,time_upload=False | 135 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.562 | prec=torch.float16,time_upload=False | 136 | | Debayer3x3 | GeForce GTX 1080 Ti | 2.812 | prec=torch.float16,time_upload=False | 137 | | Debayer5x5 | GeForce GTX 1080 Ti | 4.545 | prec=torch.float16,time_upload=False | 138 | | DebayerSplit | GeForce GTX 1080 Ti | 1.148 | prec=torch.float16,time_upload=False | 139 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 2.097 | transparent_api=False,time_upload=False | 140 | | OpenCV 4.5.3 | Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz | 11.042 | transparent_api=True,time_upload=False | 141 | 142 | ### Machine 2 143 | torch: v1.11.0+cu113 144 | pytorch-debayer: v1.1.0 145 | 146 | Method | Device | Elapsed [msec/image] | Mode | 147 | |:----:|:------:|:-------:|:----:| 148 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.232 | prec=torch.float32,time_upload=False | 149 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 1.173 | prec=torch.float32,time_upload=False | 150 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.723 | prec=torch.float32,time_upload=False | 151 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 11.959 | prec=torch.float32,time_upload=False | 152 | | Debayer2x2 | NVIDIA GeForce RTX 3090 | 0.173 | prec=torch.float16,time_upload=False | 153 | | Debayer3x3 | NVIDIA GeForce RTX 3090 | 1.067 | prec=torch.float16,time_upload=False | 154 | | Debayer5x5 | NVIDIA GeForce RTX 3090 | 1.687 | prec=torch.float16,time_upload=False | 155 | | DebayerSplit | NVIDIA GeForce RTX 3090 | 0.371 | prec=torch.float16,time_upload=False | 156 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 0.696 | transparent_api=False,time_upload=False | 157 | | OpenCV 4.5.5 | Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz | 5.216 | transparent_api=True,time_upload=False | 158 | 159 | 160 | ## Version 1.0.0 161 | 162 | Method | Device | Elapsed [msec/image] | Mode | 163 | |:----:|:------:|:-------:|:----| 164 | | Debayer2x2 | GeForce GTX 1080 Ti | 0.71 | time_upload=False,batch_size=10 | 165 | | Debayer2x2 | GeForce RTX 2080 SUPER | 0.52 | time_upload=False,batch_size=10 | 166 | | Debayer2x2 | Tesla V100-SXM2-16GB | 0.31 | time_upload=False,batch_size=10 | 167 | | Debayer3x3 | GeForce GTX 1080 Ti | 2.80 | time_upload=False,batch_size=10 | 168 | | Debayer3x3 | GeForce RTX 2080 SUPER | 2.16 | time_upload=False,batch_size=10 | 169 | | Debayer3x3 | Tesla V100-SXM2-16GB | 1.21 | time_upload=False,batch_size=10 | 170 | | DebayerSplit | GeForce GTX 1080 Ti | 2.54 | time_upload=False,batch_size=10 | 171 | | DebayerSplit | Tesla V100-SXM2-16GB | 1.08 | time_upload=False,batch_size=10 | 172 | | OpenCV 4.1.2 | CPU i7-8700K | 3.13 | transparent_api=False,time_upload=False,batch_size=10 | 173 | | OpenCV 4.1.2 | GPU GeForce GTX 1080 Ti | 1.25 | transparent_api=True,time_upload=False,batch_size=10 | --------------------------------------------------------------------------------