Repository: python-pillow/Pillow

Issue:
Incorrect Grayscale Conversion
### What did you do?

I'm having a weird inconsistency in the way Pillow is treating grayscale images, and the difference is enough to throw off some metrics I need to compute. After a lot of debugging I was able to trace the inconsistency back to the grayscale conversion. I have included a comparison with OpenCV which I have verified to be the correct conversion. By correct conversion I mean one that is consistent with other programs I have tried (they all agree with OpenCV)

It is very hard to detect this difference by just looking at the grayscale ouputs but I am including them for completeness. The difference becomes extremely apparent under aggressive JPEG compression.

##### Pillow Output (verified wrong)
![pillow_output](https://user-images.githubusercontent.com/5865387/56368408-58561980-61c5-11e9-9800-0678dc02b4e7.png)

##### OpenCV Output (verified correct)
![opencv_output](https://user-images.githubusercontent.com/5865387/56368476-8176aa00-61c5-11e9-998e-7edcd4787246.png)

##### Difference
![diff](https://user-images.githubusercontent.com/5865387/56368502-89cee500-61c5-11e9-9db4-47f963b6c6a4.png)


Here is the code used to compute these images:

```python
im = np.asarray(Image.open(args.input).convert('L'))
im2 = cv2.cvtColor(cv2.imread(args.input), cv2.COLOR_BGR2GRAY)

diff = im - im2

cv2.imwrite('pillow_output.png', im)
cv2.imwrite('opencv_output.png', im2)
cv2.imwrite('diff.png', diff)
```

**Things to Note**

* This difference is *only* apparent when I do a grayscale conversion. If I leave the images as color images they are identical
* OpenCV loads the images as BGR so line 2 is not a typo
* Maybe this is related to a difference in the chosen luma transform? 

### What are your OS, Python and Pillow versions?

* OS: Various Linux
* Python: 3.5 and 3.6
* Pillow: 6.0.0



Discussion:
Going off of https://docs.opencv.org/3.1.0/de/d25/imgproc_color_conversions.html OpenCV appears to be using the same grayscale conversion that pillow is so I really have no idea why this is happening
And based on https://github.com/cloudflare/jpegtran/blob/master/jccolor.c#L48 libjpeg does the same
Could you attach the source image that you are passing into your code?
Sorry for the delayed reply. 

First off here is more complete code that you can run to reproduce, I realized I only posted a snippet before. This thing you can run and give the image as the sole argument and it should reproduce the images I attached to the original post

```python
from PIL import Image
import cv2
from argparse import ArgumentParser
import numpy as np

parser = ArgumentParser()
parser.add_argument('input')
args = parser.parse_args()

im = np.asarray(Image.open(args.input).convert('L'))
im2 = cv2.cvtColor(cv2.imread(args.input), cv2.COLOR_BGR2GRAY)

diff = im - im2

cv2.imwrite('pillow_output.png', im)
cv2.imwrite('opencv_output.png', im2)
cv2.imwrite('diff.png', diff)
```
Next, here is the image I am using. I had to convert it to PNG for github, which is probably not a problem but it case it is, it is parrots.bmp from the live1 image quality assessment database (https://live.ece.utexas.edu/research/quality/subjective.htm).

![parrots](https://user-images.githubusercontent.com/5865387/56811035-ce7e0000-6805-11e9-9123-b297635e739e.png)




I find that changing 'L' to 'I' works.

```python
from PIL import Image
import cv2
from argparse import ArgumentParser
import numpy as np

parser = ArgumentParser()
parser.add_argument('input')
args = parser.parse_args()

im = np.asarray(Image.open(args.input).convert('I'))
im2 = cv2.cvtColor(cv2.imread(args.input), cv2.COLOR_BGR2GRAY)

diff = im - im2

cv2.imwrite('pillow_output.png', im)
cv2.imwrite('opencv_output.png', im2)
cv2.imwrite('diff.png', diff)
```
'I' is supposed to be signed integer though. Maybe it's because of an increase in precision?

I'm also noticing slight discrepancies with GIMP and mogrify as well so this might not be a major issue. I have a feeling there is some overflow that is making the difference image look worse than it is
It also works with F.

Resolving patch:
diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -134,7 +134,9 @@ def getcolor(color, mode):
 
     if Image.getmodebase(mode) == "L":
         r, g, b = color
-        color = (r * 299 + g * 587 + b * 114) // 1000
+        # ITU-R Recommendation 601-2 for nonlinear RGB
+        # scaled to 24 bits to match the convert's implementation.
+        color = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16
         if mode[-1] == "A":
             return (color, alpha)
     else:


Test patch:
diff --git a/Tests/images/imagedraw_floodfill_L.png b/Tests/images/imagedraw_floodfill_L.png
Binary files a/Tests/images/imagedraw_floodfill_L.png and b/Tests/images/imagedraw_floodfill_L.png differ
diff --git a/Tests/images/imagedraw_polygon_kite_L.png b/Tests/images/imagedraw_polygon_kite_L.png
Binary files a/Tests/images/imagedraw_polygon_kite_L.png and b/Tests/images/imagedraw_polygon_kite_L.png differ
diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py
--- a/Tests/test_image_entropy.py
+++ b/Tests/test_image_entropy.py
@@ -7,9 +7,9 @@ def entropy(mode):
             return hopper(mode).entropy()
 
         self.assertAlmostEqual(entropy("1"), 0.9138803254693582)
-        self.assertAlmostEqual(entropy("L"), 7.06650513081286)
-        self.assertAlmostEqual(entropy("I"), 7.06650513081286)
-        self.assertAlmostEqual(entropy("F"), 7.06650513081286)
+        self.assertAlmostEqual(entropy("L"), 7.063008716585465)
+        self.assertAlmostEqual(entropy("I"), 7.063008716585465)
+        self.assertAlmostEqual(entropy("F"), 7.063008716585465)
         self.assertAlmostEqual(entropy("P"), 5.0530452472519745)
         self.assertAlmostEqual(entropy("RGB"), 8.821286587714319)
         self.assertAlmostEqual(entropy("RGBA"), 7.42724306524488)
diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py
--- a/Tests/test_image_getdata.py
+++ b/Tests/test_image_getdata.py
@@ -20,9 +20,9 @@ def getdata(mode):
             return data[0], len(data), len(list(data))
 
         self.assertEqual(getdata("1"), (0, 960, 960))
-        self.assertEqual(getdata("L"), (16, 960, 960))
-        self.assertEqual(getdata("I"), (16, 960, 960))
-        self.assertEqual(getdata("F"), (16.0, 960, 960))
+        self.assertEqual(getdata("L"), (17, 960, 960))
+        self.assertEqual(getdata("I"), (17, 960, 960))
+        self.assertEqual(getdata("F"), (17.0, 960, 960))
         self.assertEqual(getdata("RGB"), ((11, 13, 52), 960, 960))
         self.assertEqual(getdata("RGBA"), ((11, 13, 52, 255), 960, 960))
         self.assertEqual(getdata("CMYK"), ((244, 242, 203, 0), 960, 960))
diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py
--- a/Tests/test_image_getextrema.py
+++ b/Tests/test_image_getextrema.py
@@ -9,14 +9,14 @@ def extrema(mode):
             return hopper(mode).getextrema()
 
         self.assertEqual(extrema("1"), (0, 255))
-        self.assertEqual(extrema("L"), (0, 255))
-        self.assertEqual(extrema("I"), (0, 255))
-        self.assertEqual(extrema("F"), (0, 255))
+        self.assertEqual(extrema("L"), (1, 255))
+        self.assertEqual(extrema("I"), (1, 255))
+        self.assertEqual(extrema("F"), (1, 255))
         self.assertEqual(extrema("P"), (0, 225))  # fixed palette
         self.assertEqual(extrema("RGB"), ((0, 255), (0, 255), (0, 255)))
         self.assertEqual(extrema("RGBA"), ((0, 255), (0, 255), (0, 255), (255, 255)))
         self.assertEqual(extrema("CMYK"), ((0, 255), (0, 255), (0, 255), (0, 0)))
-        self.assertEqual(extrema("I;16"), (0, 255))
+        self.assertEqual(extrema("I;16"), (1, 255))
 
     def test_true_16(self):
         with Image.open("Tests/images/16_bit_noise.tif") as im:
diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py
--- a/Tests/test_image_histogram.py
+++ b/Tests/test_image_histogram.py
@@ -8,9 +8,9 @@ def histogram(mode):
             return len(h), min(h), max(h)
 
         self.assertEqual(histogram("1"), (256, 0, 10994))
-        self.assertEqual(histogram("L"), (256, 0, 638))
-        self.assertEqual(histogram("I"), (256, 0, 638))
-        self.assertEqual(histogram("F"), (256, 0, 638))
+        self.assertEqual(histogram("L"), (256, 0, 662))
+        self.assertEqual(histogram("I"), (256, 0, 662))
+        self.assertEqual(histogram("F"), (256, 0, 662))
         self.assertEqual(histogram("P"), (256, 0, 1871))
         self.assertEqual(histogram("RGB"), (768, 4, 675))
         self.assertEqual(histogram("RGBA"), (1024, 0, 16384))
diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py
--- a/Tests/test_imagecolor.py
+++ b/Tests/test_imagecolor.py
@@ -165,14 +165,14 @@ def test_rounding_errors(self):
 
         self.assertEqual(0, ImageColor.getcolor("black", "L"))
         self.assertEqual(255, ImageColor.getcolor("white", "L"))
-        self.assertEqual(162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "L"))
+        self.assertEqual(163, ImageColor.getcolor("rgba(0, 255, 115, 33)", "L"))
         Image.new("L", (1, 1), "white")
 
         self.assertEqual(0, ImageColor.getcolor("black", "1"))
         self.assertEqual(255, ImageColor.getcolor("white", "1"))
         # The following test is wrong, but is current behavior
         # The correct result should be 255 due to the mode 1
-        self.assertEqual(162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1"))
+        self.assertEqual(163, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1"))
         # Correct behavior
         # self.assertEqual(
         #     255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1"))
@@ -180,5 +180,5 @@ def test_rounding_errors(self):
 
         self.assertEqual((0, 255), ImageColor.getcolor("black", "LA"))
         self.assertEqual((255, 255), ImageColor.getcolor("white", "LA"))
-        self.assertEqual((162, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA"))
+        self.assertEqual((163, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA"))
         Image.new("LA", (1, 1), "white")