robweber / omni-epd

An EPD (electronic paper display) class abstraction to simplify communications across multiple display types.
GNU General Public License v3.0
83 stars 19 forks source link

only red pixels #58

Closed donbing closed 2 years ago

donbing commented 2 years ago

Hi, I've got a waveshare_epd.epd2in7b_V2 that i'm trying to show a picture on.

when i configure the device for red mode using an ini file all I get is red pixels on the display, should it also display black?

e.g: source image: origonal resized and converted to RBG: converted displayed: rendered

robweber commented 2 years ago

Thanks for pointing this out, you're right there is definitely an issue there.

I think I have it figured out but if you could please run a test on your system that would be great. I don't have your specific device but using the omni_epd.mock device with a custom palette I was able to to determine two different things were happening during the color filtering functions:

  1. images not currently in the RGB mode need to be converted to that mode before filtering
  2. The wrong variable was being referenced when backfilling the unused colors

The result of both of these was that white was essentially being interpreted as black on the image. On the EPD this meant that anything black would be drawn as white when in the 3 color mode. BW only images were unaffected since they aren't using color filtering, simply converted to 2 color.

The fix should be in this commit. If you could update your local install and rebuild the library it should work now - I hope!

aaronr8684 commented 2 years ago

I'm happy to test on my 7.5b_V2 if needed. I also have a 4.2c screen on it's way. Hopefully this wasn't caused by the testing we did earlier.

donbing commented 2 years ago

I'm going to test on my 2.7 waveshare and 4.2 inky once I can get numpy to download correctly, the setup seems to be getting stuck there for me atm.

donbing commented 2 years ago

ok, got it to install correctly on my Pi4, but I get the following error from the test util

pi@raspberrypi:~/omni-epd $ omni-epd-test -e inky.what_red
Loaded inky.what_red with width 400 and height 300
Drawing rectangle of width 300.0 and height 225.0
Traceback (most recent call last):
  File "/usr/local/bin/omni-epd-test", line 10, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.7/dist-packages/omni_epd/test_utility.py", line 127, in main
    test.draw()
  File "/usr/local/lib/python3.7/dist-packages/omni_epd/test_utility.py", line 83, in draw
    draw = self.__draw_rectangle(draw, self.epd.width, self.epd.height, 0, 0, .75, .25)
  File "/usr/local/lib/python3.7/dist-packages/omni_epd/test_utility.py", line 58, in __draw_rectangle
    imgObj.rectangle((rX, rY, rWidth + rX, rHeight + rY), outline=ImageColor.getrgb("black"), width=2)
TypeError: rectangle() got an unexpected keyword argument 'width'

both my pi zero2s are getting stuck at the numpy install

pi@comitup-624:~/omni-epd $ sudo pip3 install .
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Processing /home/pi/omni-epd
  Installing build dependencies ... done
Collecting Pillow (from omni-epd==0.2.7b1)
  Using cached https://www.piwheels.org/simple/pillow/Pillow-9.0.1-cp37-cp37m-linux_armv7l.whl
Collecting waveshare-epd@ git+https://github.com/waveshare/e-Paper.git#subdirectory=RaspberryPi_JetsonNano/python&egg=waveshare-epd from git+https://github.com/waveshare/e-Paper.git#subdirectory=RaspberryPi_JetsonNano/python&egg=waveshare-epd (from omni-epd==0.2.7b1)
  Cloning https://github.com/waveshare/e-Paper.git to /tmp/pip-install-lsb5upt7/waveshare-epd
Collecting inky[rpi] (from omni-epd==0.2.7b1)
  Using cached https://files.pythonhosted.org/packages/9e/db/ab689bad7935ce13cf0356bf2ad6e13ee2a115f3a33de7596676ad464921/inky-1.3.1-py3-none-any.whl
Collecting hitherdither@ git+https://github.com/hbldh/hitherdither from git+https://github.com/hbldh/hitherdither (from omni-epd==0.2.7b1)
  Cloning https://github.com/hbldh/hitherdither to /tmp/pip-install-lsb5upt7/hitherdither
Requirement already satisfied: RPi.GPIO in /usr/lib/python3/dist-packages (from waveshare-epd@ git+https://github.com/waveshare/e-Paper.git#subdirectory=RaspberryPi_JetsonNano/python&egg=waveshare-epd->omni-epd==0.2.7b1) (0.7.0)
Collecting numpy (from waveshare-epd@ git+https://github.com/waveshare/e-Paper.git#subdirectory=RaspberryPi_JetsonNano/python&egg=waveshare-epd->omni-epd==0.2.7b1)
  Using cached https://files.pythonhosted.org/packages/c2/a8/a924a09492bdfee8c2ec3094d0a13f2799800b4fdc9c890738aeeb12c72e/numpy-1.21.5.zip
  Installing build dependencies ... done
Requirement already satisfied: spidev in /usr/lib/python3/dist-packages (from waveshare-epd@ git+https://github.com/waveshare/e-Paper.git#subdirectory=RaspberryPi_JetsonNano/python&egg=waveshare-epd->omni-epd==0.2.7b1) (3.5)
Collecting smbus2 (from inky[rpi]->omni-epd==0.2.7b1)
  Using cached https://files.pythonhosted.org/packages/c8/bf/62ef029fb7077fc87c3539f7365859bccc6cedb2bb20796b737b788c8d09/smbus2-0.4.1-py2.py3-none-any.whl
Building wheels for collected packages: omni-epd, waveshare-epd, hitherdither, numpy
  Running setup.py bdist_wheel for omni-epd ... done
  Stored in directory: /root/.cache/pip/wheels/45/58/69/3b9460b88cc0aee0ed20ed6e27c300b100ddac04e4d43d261f
  Running setup.py bdist_wheel for waveshare-epd ... done
  Stored in directory: /tmp/pip-ephem-wheel-cache-d_a3y7oj/wheels/4b/4d/5a/fc28bbb9a5787d248880f44b98931eab72d5d884de27a4845b
  Running setup.py bdist_wheel for hitherdither ... done
  Stored in directory: /tmp/pip-ephem-wheel-cache-d_a3y7oj/wheels/2d/7f/db/ba7bd2317fae19a700c11482577b6115f907ff836b2221c184
  Running setup.py bdist_wheel for numpy ... /
donbing commented 2 years ago

hm, the pi4 has PIL 4.2.1 installed. that's why :(

pi@raspberrypi:~/omni-epd $ python3
Python 3.7.3 (default, Jan 22 2021, 20:04:44)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> PIL.version.__version__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'PIL' is not defined
>>> import PIL
>>> PIL.version.__version__
'4.2.1'

but setup indicates that it found 5.4.1

pi@raspberrypi:~/omni-epd $ sudo pip3 install .
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Processing /home/pi/omni-epd
  Installing build dependencies ... done
Requirement already satisfied: Pillow in /usr/lib/python3/dist-packages (from omni-epd==0.2.7b1) (5.4.1)
more /usr/lib/python3/dist-packages/PIL/_version.py
::::::::::::::
# Master version for Pillow
__version__ = '5.4.1'
donbing commented 2 years ago

I think setup.cfg needs to pin it's dependencies.. PR to fix that here https://github.com/robweber/omni-epd/pull/59

donbing commented 2 years ago

so after fixing that, and running the draw_image example, I only get black pixels on the red inkywhat

pi@raspberrypi:~/omni-epd/examples/basic_example $ python3 draw_image.py
Loading display
Loading image
Writing to display

20220226_195145

donbing commented 2 years ago

i added the following ini file to the example

[EPD]
type=inky.what_red
mode=red

[Display]
rotate=180

[Image Enhancements]
contrast=1.5
sharpness=2

and now i get bits of red in the pic! 20220226_195649

i'll try with the waveshare a bit later, to verify that it works there too.

donbing commented 2 years ago

looks right on the waveshare_epd.epd2in7b_V2 too!

20220226_204227

donbing commented 2 years ago

altho.. why does one have a black background and one white :)

missionfloyd commented 2 years ago

Looks like waveshare forgot to invert the black layer.

https://github.com/waveshare/e-Paper/blob/ba202c58ec5a26bbc3dad4d3065ec597d730f373/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in7b_V2.py#L153

robweber commented 2 years ago

Interesting....it's easily fixed by flipping how the image is generated but wonder how many other displays require the inverted image?

Is this an oversight or intentional in the Waveshare driver do you think? If it's intentional we'll have to fix here.

aaronr8684 commented 2 years ago

@robweber Well I think I have an answer to that question and why I was seeing weird behavior on my 7.5b in the other issues. I went through every driver file in the WS lib and wow is it inconsistent (as you know). Anyway, I added to my already existing google sheet some additional info on if each driver does inverting in the buffer and then possibly again in the display methods.

For reference, here are the ways that they invert (or it seems). I really didn't dig into the code a ton. I just marked it if it appeared to be inverting the bits:

v1.1 - buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8)) - invert v1.2 - buf[int(x / 8) + y * linewidth] &= ~(0x80 >> (x % 8)) - invert v2 - buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2)) - invert and grey to red v3 - buf[i] ^= 0xFF v4 - data_t = buf[Add]&(~(0xF0 >> ((newx % 2)*4))) - 4" color

The above "versions" of inverting are what I reference in my sheet here (drivers tab) - https://docs.google.com/spreadsheets/d/1r4EKPGZvz_xQZA58WPyuTVPhXGKCWd7-Z-btqgaFfKw/edit#gid=1378358670

I tried to highlight differences from the norm, so outliers were more easily identified.

Sorry it took me so long to respond, but I wanted to have some concrete info before I speculated

missionfloyd commented 2 years ago

I think it's probably an oversight. Usually, with e-paper 1 is black and 0 is white.

@aaronr8684 does waveshare's example display correctly?

aaronr8684 commented 2 years ago

On my 7.5b? Yea. I ran a bunch of tests with Omni with it as well. I know that a black image pixel is on for the black, red, or yellow ePaper pixels, but it seems like every driver handles it in a different way. I'm not really sure at this point what the best approach is for Omni, but I figured the first step was documenting the differences.

missionfloyd commented 2 years ago

No, the 2.7b. If the waveshare example displays incorrectly as well (the background should be white), then the line I linked to above should be changed to self.send_data(~imageblack[i])

Waveshare's drivers could really use some work.

aaronr8684 commented 2 years ago

@missionfloyd I don't have the 2.7b. Yes, a little consistency would be nice. They really should have one method of display per screen type. BW, R/Y, 4-Grey, Color

@donbing have you tried the v1 waveshare_epd.epd2in7b driver?

donbing commented 2 years ago

@aaronr8684 the non-V2 version? if so, no. but I will

donbing commented 2 years ago
pi@raspberrypi:~/omni-epd/examples $ omni-epd-test -e waveshare_epd.epd2in7b -i PIA03519_small.jpg 
Loaded waveshare_epd.epd2in7b with width 176 and height 264

... the command never terminates, and the screen is not refreshed (my device is a v2)

donbing commented 2 years ago

I did a bit more testing..

e.g. 1. red mode gives white background

Given this omni-epg.ini

[EPD]
type=waveshare_epd.epd2in7b_V2
mode=red

When I execute

omni-epd-test -e waveshare_epd.epd2in7b_V2 -i PIA03519_small.jpg

Then I get this image 20220228_192240

e.g. 2. BW mode gives black background

Given this omni-epg.ini

[EPD]
type=waveshare_epd.epd2in7b_V2

When I execute

omni-epd-test -e waveshare_epd.epd2in7b_V2 -i PIA03519_small.jpg

Then I get this image 20220228_191528

donbing commented 2 years ago

Testing on a 4.2" red inkywhat gives back backgrounds for both red and BW.

donbing commented 2 years ago

Testing on a 5.6" inky impression gives black backgrounds for both color and BW modes too

donbing commented 2 years ago

I also have a waveshare pico-epaper-4.2. But that's a faff to wire up, so I'll leave it unless anyone specifically wants it testing..

aaronr8684 commented 2 years ago

@donbing Can you modify the Omni-EPD library code in /usr/local/lib/python<ver>/dist-packages/omni_epd/displays/waveshare_display.py on line 214 to img_black.putpalette((255, 255, 255, 0, 0, 0))

You'll need to change <ver> to your version, either 3 or 3.X

Does that fix the background in "red" mode?

aaronr8684 commented 2 years ago

In summary, the issue lies in how the individual libraries handle the image to buffer conversion. The correct result (that you see in some waveshare libraries and all tested inky libraries) is to convert the black in an image (0x00) to ePaper black (0xFF). The Omni library is correctly generating the black background PIL image (see attached), but some of the libraries are not inverting it correctly before sending it to the screen. The correct fix is to create a PR in the official repo for waveshare, but the faster fix might be to figure out which screens are incorrect and break them out into a separate class implementation.

img_black.show() output: img_black.show() output

aaronr8684 commented 2 years ago

I also have a waveshare pico-epaper-4.2. But that's a faff to wire up, so I'll leave it unless anyone specifically wants it testing..

Yea my 5.65" color is a pico hat, that I have to wire up to work with my pi4. After I did it once, I just snapped a photo so that I didn't have to work with the pin table and could just visually see the pinout. I've attached it if it helps. Ignore the 5V/Ground fan wires 😄

Visual Pinout (uses 3.3V): 2022-02-06 21 15 35

donbing commented 2 years ago

@aaronr8684 I changed the code you suggested. And yep, I now get black backgrounds for both black and red modes

aaronr8684 commented 2 years ago

@donbing I assumed that would work. It shouldn't have any effect on the bw mode, but it's helpful to test. Thanks!

The epd5in83c also behaves like your 2.7b_V2 so that's at least 2 screens that have been confirmed to have a different implementation. I have someone testing the 2.7b_V1 to see if it's the same.

I think at this point, we'll have to wait to see how @robweber would like to approach it. Once a direction is given, I'm happy to do the coding if no one has the time at the moment. I'm working on adding Omni to another project, so I'm glad that we are working through these bugs first to make testing downstream a little smoother.

aaronr8684 commented 2 years ago

@aaronr8684 I changed the code you suggested. And yep, I now get black backgrounds for both black and red modes

If you're curious, this is what the img_black.show() output looks like with the code you changed: img_black-putpalette-white255_then_black0

donbing commented 2 years ago

Glad I could help 👍, I'd been looking at adding omni to my crypto-ticker project.

I really appreciate the pic btw, I've got the 4.2" pico red up and running with the waveshare_epd.epd4in2b_V2 driver 🤩.

donbing commented 2 years ago

You'll be happy to know that waveshare_epd.epd4in2b_V2 also suffers from the black-conversion issue 🤕

aaronr8684 commented 2 years ago

Glad I could help 👍, I'd been looking at adding omni to my crypto-ticker project.

Yea I was taking a look at your project and really like the look of it. I was thinking it would be cool to bring in that functionality to the epd_display / PaperPi project as a plugin #59. It would be a great addition and offers a lot more features over the current crypto option. Maybe someday when all the other work is done 🤷‍♂️

robweber commented 2 years ago

The correct fix is to create a PR in the official repo for waveshare, but the faster fix might be to figure out which screens are incorrect and break them out into a separate class implementation.

Really appreciate all the testing everyone is putting into this. I never would have thought a simple "red isn't showing up" issue would have ballooned so much. I think the best approach at this point might be to simply handle the screens that are incorrect. In the device mapping another key could be added to specify which ones and then use the correct palette filter to get the right image for the buffer. To be fair I hate this type of solution since as soon as Waveshare changes it will be out of date but it's a compromise on usability.

@aaronr8684 - that spreadsheet is nice, must have taken some time to dig into all the displays. Is that something I could link to on the Wiki of this project? More for informational than anything.

aaronr8684 commented 2 years ago

You'll be happy to know that waveshare_epd.epd4in2b_V2 also suffers from the black-conversion issue 🤕

I added another column to track which screens display correctly and which do not. More info the better, right? 😅

aaronr8684 commented 2 years ago

@aaronr8684 - that spreadsheet is nice, must have taken some time to dig into all the displays. Is that something I could link to on the Wiki of this project? More for informational than anything.

@robweber Absolutely! It's not at all "cleaned up" and although I tried to be as accurate as possible, I'm sure some of the code interpretation is probably wrong from my "at a glance" approach. I've change the read-only access to comment access so it can be more of a collaborative document. I'll also add the versions of inversion to the doc for easier reference.

missionfloyd commented 2 years ago

@aaronr8684 Sorry, I responded to the wrong person.

@donbing Have you checked if waveshare's examples work right? It would help to know if it's our problem or theirs.

aaronr8684 commented 2 years ago

@donbing Sorry for the double ping. A photo would be nice if you run the waveshare test file on the 3rd test

donbing commented 2 years ago

np, here's a few snaps from the waveshare example.

20220301_095820 20220301_095855 20220301_095912

aaronr8684 commented 2 years ago

@donbing Will you try one more thing with the Omni test file after making the following changes?

In /usr/local/lib/python<ver>/dist-packages/omni_epd/displays/waveshare_display.py: Replace this line (should be 214): img_black.putpalette((0, 0, 0, 255, 255, 255))

With these:

            img_black = img_black.convert('L')
            threshold = 32
            img_black = img_black.point( lambda p: 255 if p > threshold else 0)
            img_black = img_black.convert('1')

And then share the results.

Thanks in advance!

aaronr8684 commented 2 years ago

I ran some tests with different threshold values...the higher the number the more black that will result. In the filtered image, the only difference seems to be whether the red is converted to black or white, which makes sense. I think the red in the original filtered image must be represented somewhere between 64 and 128, probably around 85 if I had to guess.

224 and 128: 224

64 and 32: 64

For bw images, 128 is probably good, but with a third color, I think the best results are going to be in the lower range.

missionfloyd commented 2 years ago

Looks like the driver is correct. Are we sure 827321789983b9368f5e0f8d68421eec13bc6f40 didn't break something?

@donbing can you try changing lines 214 and 217 in waveshare_display.py back to img_black.putpalette((255, 255, 255, 0, 0, 0, 255, 255, 255) + (255, 255, 255)*253) and img_color.putpalette((255, 255, 255, 255, 255, 255, 0, 0, 0) + (255, 255, 255)*253) respectively?

Alternatively, what if we convert back to RGB, then let the driver do it's thing?

Change line 219 to

self._device.display(self._device.getbuffer(img_black.convert("RGB")), self._device.getbuffer(img_color.convert("RGB")))
donbing commented 2 years ago

img_color.putpalette((255, 255, 255, 255, 255, 255, 0, 0, 0) + (255, 255, 255)*253)

line 217 is already identical to this?

donbing commented 2 years ago

I changed just line 214, to read

img_black.putpalette((255, 255, 255, 0, 0, 0, 255, 255, 255) + (255, 255, 255)*253)

20220302_131124

donbing commented 2 years ago

however.. i then change it back to

img_black.putpalette((255, 255, 255, 0, 0, 0))

and I still get a black background, this is using the test_utility code

donbing commented 2 years ago

using

img_black.putpalette((0, 0, 0, 255, 255, 255))

results in a white background

aaronr8684 commented 2 years ago

@donbing were you able to make the changes I suggested?

donbing commented 2 years ago

@aaronr8684 will try now, 2 mins

donbing commented 2 years ago

Alrighty, so Given:

            img_black = img_black.convert('L')
            threshold = 32
            img_black = img_black.point( lambda p: 255 if p > threshold else 0)
            img_black = img_black.convert('1')

Then: 20220302_132650

aaronr8684 commented 2 years ago

Looks like the driver is correct. Are we sure 8273217 didn't break something?

It's possible (probable even) but at this point it doesn't matter, because regardless of which way the putpalette is setup, it break some screens. I'm trying to test some solutions that will work across all screen, and based on donbing's last reply, that last test might have done that. @missionfloyd do you have any red or yellow screens that you can test with the fix of replacing the putpalette?

aaronr8684 commented 2 years ago

That's great @donbing, that means that this solution works on yours and mine which have had the opposite results with the putpalette options we've tested so far.

For completeness of the solution (and to incorporate some changes I've tested since), can you replace the entire else block starting around line 208 with the following code:

            # apply the color filter to get a 3 color image
            image = self._filterImage(image)
            image = image.convert('L')

            # create copies to send separately to the display method
            img_black = image.copy()
            img_color = image.copy()

            # switch grey colors to black or white based on threshold
            threshold = 16
            img_black = img_black.point( lambda p: 255 if p > threshold else 0 )

            # switch grey colors to black or white based on hardcoded thresholds. Should work with red or yellow screens
            img_color = img_color.point( lambda p: 0 if p > 18 and p < 235 else 255 )

            self._device.display(self._device.getbuffer(img_black), self._device.getbuffer(img_color))