diff --git a/README.md b/README.md index 978793b..ee06d66 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ A node that that will analyze the differences between a modified QR and create a * `module_size` - The size in pixels of each QR module (the individual blocks that make up a qr code) in the provided images. * `grayscale_method` - How to convert the RGB color channels to a single grayscale value. * `mean` - A simple average of the RGB values. + * `luminance` - Converts colors to grayscale using a [more advanced formula](https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale) to better match percieved brightness. * `aggregate_method` - How to aggregate the many pixels that make up a QR module into a single value. * `mean` - Within each module a simple average is taken of all constituent pixels. * `evaluate` - Which part of the QR to examine. @@ -107,6 +108,7 @@ A node that that will analyze the differences between a modified QR and create a * `finder_pattern` - Ignores the modules while examining only the 3 finder patterns at the corners of the QR. * `error_difficulty` - It is possible that while using the same reader a QR code could be easily scannable on one monitor, but not on another. The way the colors are displayed can have a major effect on unsability. By increasing this threshold, you are simulating inability to distinguish midrange pixels for both the `PERCENT_ERROR` and `QR_ERROR_MASK` outputs. It ranges from 0 to 1 where 0 only automatically fails perfect grays while 1 accepts only exactly matched modules that are perfectly black or white. * `inverted_pattern` - If set to `True` this assumes that the QR code has inverted colors (a black background with white modules.) +* `gamma` - If using the `grayscale` method based on `luminance`, this will influence the formula by appending the gamma expansion and compression formulas before and after the color conversion. A gamma of 2.2 will use the [formula specific to srgb](qr_nodes.py#0.04045), while other gamma values will simply use the gamma amount as an exponent. #### Outputs diff --git a/qr_nodes.py b/qr_nodes.py index 10824e8..b7334e8 100644 --- a/qr_nodes.py +++ b/qr_nodes.py @@ -273,11 +273,12 @@ class QRErrorMasker: "source_qr": ("IMAGE",), "modified_qr": ("IMAGE",), "module_size": ("INT", {"default": 16, "min": 1, "max": 64, "step": 1}), - "grayscale_method": (["mean",], {"default": "mean"}), + "grayscale_method": (["mean", "luminance"], {"default": "luminance"}), "aggregate_method": (["mean",], {"default": "mean"}), "evaluate": (["full_qr", "module_pattern", "finder_pattern"], {"default": "module_pattern"}), "error_difficulty": ("FLOAT", {"default": 0, "min": 0, "max": 1, "step": .01}), "inverted_pattern": ("BOOLEAN", {"default": False}), + "gamma": ("FLOAT", {"default": 2.2, "min": .1, "max": 2.8, "step": .1}), }, } @@ -319,6 +320,28 @@ class QRErrorMasker: def _squeeze_by_mean(self, tensor): return torch.mean(tensor, dim=-1) + def _gamma_expansion(self, tensor, gamma): + if gamma == 1: + return tensor + if gamma == 2.2: + return torch.where(tensor <= 0.04045, tensor / 12.92, ((tensor + 0.055) / 1.055) ** 2.4) + return tensor ** gamma + + def _gamma_compression(self, tensor, gamma): + if gamma == 1: + return tensor + if gamma == 2.2: + return torch.where(tensor <= .0031308, tensor * 12.92, 1.055 * tensor ** (1/2.4) - 0.055) + return tensor ** (1/gamma) + + def _grayscale_by_luminance(self, tensor, gamma): + weights = torch.tensor([0.2125, 0.7154, 0.0721], dtype=torch.float32) + tensor = self._gamma_expansion(tensor, gamma) + tensor = tensor @ weights + if gamma != 1: + tensor = tensor ** gamma + return self._gamma_compression(tensor, gamma) + def _squeeze_to_modules(self, tensor, method): tensor = self._reshape_tensor_to_modules(tensor) if method == "mean": @@ -332,7 +355,8 @@ class QRErrorMasker: module_size, grayscale_method, aggregate_method, - inverted_pattern + inverted_pattern, + gamma ): if source_qr.shape != modified_qr.shape: raise ValueError("Source and modified QR must have the same dimensions.") @@ -348,6 +372,8 @@ class QRErrorMasker: ) if grayscale_method == "mean": modified_qr = self._squeeze_by_mean(modified_qr) + elif grayscale_method == "luminance": + modified_qr = self._grayscale_by_luminance(modified_qr, gamma) else: raise ValueError("Currently only mean is supported for rgb to grayscale conversion.") source_qr = self._squeeze_to_modules(source_qr, "mean") @@ -423,13 +449,15 @@ class QRErrorMasker: evaluate, error_difficulty, inverted_pattern, + gamma, ): source_qr, modified_qr = self._reduce_to_modules(source_qr, modified_qr, module_size, grayscale_method, aggregate_method, - inverted_pattern + inverted_pattern, + gamma ) mask = self._create_qr_mask(source_qr.shape[0], evaluate) error_mask, percent_error = self._compare_modules(source_qr,