Need JS expertise on function to grayscale an image

trying to find a JS script/function for gray scaling an entity_picture I found How to Turn Web Images to Grayscale (3 Ways) - Hongkiat and the 2d method there seems to come close to what I need.

Ive tried to configure it as follows, so I can use it in a picture customization (using custom-ui) but I cant get it to work.

    device_tracker.unifi_mijn_mobiel:
      entity_picture: >
          [[[ function gray(imgObj) {
                var canvas = document.createElement('canvas');
                var canvasContext = canvas.getContext('2d');

                var imgW = imgObj.width;
                var imgH = imgObj.height;
                canvas.width = imgW;
                canvas.height = imgH;

                canvasContext.drawImage(imgObj, 0, 0);
                var imgPixels = canvasContext.getImageData(0, 0, imgW, imgH);

                for(var y = 0; y < imgPixels.height; y++){
                    for(var x = 0; x < imgPixels.width; x++){
                        var i = (y * 4) * imgPixels.width + x * 4;
                        var avg = (imgPixels.data[i] + imgPixels.data[i + 1] + imgPixels.data[i + 2]) / 3;
                        imgPixels.data[i] = avg;
                        imgPixels.data[i + 1] = avg;
                        imgPixels.data[i + 2] = avg;
                    }
                }
                canvasContext.putImageData(imgPixels, 0, 0, 0, 0, imgPixels.width, imgPixels.height);
                return canvas.toDataURL();
              }
             return gray('/local/family/mijn_mobiel.jpg'); ]]]

When I test this in a button-card an error is displayed:

button-card.js?v=3.4.2:425 ButtonCardJSTemplateError: TypeError: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(CSSImageValue or HTMLCanvasElement or HTMLImageElement or HTMLVideoElement or ImageBitmap or OffscreenCanvas or SVGImageElement or VideoFrame)'

maybe this is not possible at all, but Id love to get feedback from a fellow community member with JS expertise.

Btw, I know I can simply use a grayscale filter in button cards, but this is not for that use case.

Hope you can have a look, Thanks!

Not a js expert, but what stands out to me is that you’re passing in a url of your image and expecting it to act like an Image object.

EDIT: See line 1? You’re skipping that.

var imgObj = document.getElementById('js-image');

that’s creating an image object fro the gray function.

In your case, you need to google how to create an Image object from a url and pass that to gray.

Petro is correct. That function expects an image object, but you’re giving it a URL. The drawImage call fails, because text URLs don’t have images :slight_smile: You need to supply it an actual image element. That can either be an image tag that is already part of the DOM (the webpage basically) or, if you don’t want/have one, it can also be an offscreen image buffer that you must create beforehand.

It really depends on what you want to achieve with this. Maybe you’re overengineering it and a simple CSS style could do the trick.

thanks both of you.
i had been looking t do:

           [[[ var imgObj = document.getElementById('/local/family/mijn_mobiel.jpg');
                function gray(imgObj) {
                var canvas = document.createElement('canvas');
                var canvasContext = canvas.getContext('2d');
                var imgW = imgObj.width;
                var imgH = imgObj.height;
                canvas.width = imgW;
                canvas.height = imgH;

                canvasContext.drawImage(imgObj, 0, 0);
                var imgPixels = canvasContext.getImageData(0, 0, imgW, imgH);

                for(var y = 0; y < imgPixels.height; y++){
                    for(var x = 0; x < imgPixels.width; x++){
                        var i = (y * 4) * imgPixels.width + x * 4;
                        var avg = (imgPixels.data[i] + imgPixels.data[i + 1] + imgPixels.data[i + 2]) / 3;
                        imgPixels.data[i] = avg;
                        imgPixels.data[i + 1] = avg;
                        imgPixels.data[i + 2] = avg;
                    }
                }
                canvasContext.putImageData(imgPixels, 0, 0, 0, 0, imgPixels.width, imgPixels.height);
                return canvas.toDataURL();
              }
             imgObj.src = gray(imgObj); ]]]

but that doesnt work either.
And yes, this is probably overengineering, and I dont even know if custom-ui can support this, thats why I also tried it in a custom: button-card.

what I want to do ultimately is

    device_tracker.unifi_mijn_mobiel:
      templates:
        entity_picture: >
          return (state == 'home') ? '/local/family/mijn_mobiel.jpg' : grey('/local/family/mijn_mobiel.jpg');

this is the format of the custom-ui templates, but the grey() obviously is pseudo code for now…What I do now is create 2 images for each devices state and simply do

    device_tracker.unifi_mijn_mobiel:
      templates:
        entity_picture: >
          return  '/local/family/mijn_mobiel_' + state +.jpg'

I figured Id try to cut on the overhead on images, and ‘calculate’ the not_home ones to gray :wink:

following Petro I went to Using files from web applications - Web APIs | MDN but none of that helps me for now.
Im also probably thrown off by the fact this doesnt use a ‘return’ in the last line, wwwhihc my templates always have, or any other template in button-card for that matter. So I am not even sure it is trying to return (show) that in the first place. Odd thing is I dont get any console error…

That won’t work. getElementById will search the DOM for an existing element (for example an img tag) and return it. You can’t just give it a URL, it doesn’t know how to handle this.

For example, if you have the following tag as part of your page / card:

<img id='myimage' src='/local/whatever.png' />

Then calling getElementById like this would return this image tag object:

var imgObj = document.getElementById('#myimage');

But this assumes that the image tag (which actually handles all the URL image loading and rendering) already exists in your document. If you don’t have an element like this (or you don’t want one), then loading the image manually into a memory buffer can be an alternative:

let image = new Image();
image.src = url;

Note that this is an async operation and you will need to run this in an asynchronous context and use callbacks to initiate your greyscale code when the image is actually retrieved and loaded. You can’t just dump it in there like that.

Yet another alternative is to create an image tag and add it to the DOM:

var imgObj = document.createElement('img');
imgObj.src = '/local/whatever.png';

That is easier because it can be used in sync without callbacks, but it can have some sideeffects on page rendering. You might have to play around with display styles to hide it.

Because your example doesn’t actually return anything, it modifies the existing image tag in place (replacing it with the grey version).

thanks! got a lot to learn here…
if Id use that in the gray script I posted and replace the first line with that, how then would it change the bottom line, where the actual function gray() is used?

fwiw, I am now testing the script code in a custom:button-card, to get the same result as:

    - type: custom:button-card
      name: Picture
      show_entity_picture: true
      entity_picture: /local/family/mijn_mobiel.jpg
      styles:
        img_cell:
          - -webkit-filter: grayscale(1)

in the format of

    - type: custom:button-card
      name: Picture
      show_entity_picture: true
      entity_picture: >
         [[[ full script above ]]]

If I could get that to work, and know it to be completely correct because of that, I can test it in a custom-ui customization,

I don’t know, this depends on what the template expects as a result. I have no experience trying to shoehorn JS into the frontend YAML (I didn’t know this was even possible). It looks like it expects a URL to the temporary canvas ? You need to check the docs to know what the frontend or custom card expects here.

You also have to clean up after yourself in your code. You’re creating and adding elements to the DOM with every call to the function (the canvas element and possibly the img tag), but you are never removing them. This will lead to issues later on (memory leaks, the page becoming slower and slower in the browser if you leave it open, etc).

card_mod:
  style: |
    ha-card {
      -webkit-filter: grayscale(1);
    }

Works in Picture-entity-card

PS: Sorry, the picture-entity-card i tried on, in kind of special, kind of mix, with “background frame and Xis etc” and “Plots” defined, in .py

From Tibber: Custom integration

I’ve just played around with it, because i didn’t like the default “Pic”

the integration create a png , from “statics info” and update this with plots/fill etc when receiving “state” from sensor

But the card-mod is actually applied to the “PNG”, that is located in /config/local
Edit: And it does work on default picture-entity-card, so most likely also on other cards, so maybe just “add” style to “state == home”
Cool, works on “Statistics Graph Card” also, and custom-slider-button-card, with gradient background :slight_smile:

ofc I know about that filter, as I posted in the opening post. This is not about a card_mod able entity in the frontend or a button-card. I am seeking to customize an entity.

Given Alex’s last comment, I believe it is best to leave it at that…even if it might be possible, it is becoming such a complex thing, I am better off with the straight solution: having 2 images for the 2 states and use the simple template

      entity_picture: >
          return  '/local/family/mijn_mobiel_' + state +.jpg'

thanks for chiming in everyone, appreciated!

Ok, maybe i just didn’t interpret “overhead on images” right, or the reasons you wanted to use 2nd example in “The tutorial” , but interesting to “follow” this post , iv’e been sticking to rather “old” web-server “code” for ages ( as it’s only for private use ) … haven’t really followed the development of either CSS or JS … before last year, when starting with HA

Why aren’t you applying the grayscale with a template based on the state? I don’t understand why you need 2 images.

well, yes, thats what I wanted to do:

   device_tracker.unifi_mijn_mobiel:
      templates:
        entity_picture: >
          return (state == 'home') ? '/local/family/mijn_mobiel.jpg' : grey('/local/family/mijn_mobiel.jpg');

however, for that I need to use such a complex set of manipulations in JS, as you said, create an image object from a url. I wasnt able to find a way to do so honestly. And even if I could have done so, Alex warned me to be very careful because of memory issues that could arise.

In the end it seems simpler to use the 2 local images…

Still, if you would know how to do as I had hoped for and have some more detailed suggestions, Id be happy to investigate further (tbh, I cant stand the fact I cant make it happen.)

Maybe the posted gray() template is overly complex and I could use some leaner function. Havent found anything related on the web just yet though.

You’d just do it in card mod, not on the entity_picture attribute on the CSS properties associated with the picture entity. And worse case, if it’s a built in card and it doesn’t have the ability to do this, then replace it with a custom button card.

sure, I can and will do that. As a matter of fact, I have a whole set of button-cards doing that, and then some, based on the presence state (traveling, abroad, home/not_home, country flag etc etc)

this was about customizing the entity_picture we set in customize: (too bad we can not set an entity_picture: in the Ui btw, only icon: , seems so basic).

More-over, doing it via customize, touches the entity itself, while all other frontend ways only change the frontend representation of the entity. It has been frustrating me ever since the introduction of Lovelace, that the more-info (where the true entity attributes are shown, With their customizations in templates and colors) is different from the Lovelace representation of the same entity. Or the Logbook for that matter, where customizations cause issues too. (eg customized icons/pictures are not shown)

Anyways, Ill take you suggestion at hart and create those card_mods for the device_tracker entities in an entities card too.

      - entity: device_tracker.unifi_calltheboss
        card_mod:
          style:
            hui-generic-entity-row:
              $: |
                state-badge {
                  filter: {{(states(config.entity) == 'home')|iif('grayscale(1)','none')}};
                }

mod is working fine in Frontend:

more-info:

btw, and chiming in @Ildar_Gabdullin here because this really is a nice touch I found out simply by giving it a try:

  - type: custom:fold-entity-row
    head:
      type: section
      label: Wifi
      card_mod: *label
    group_config:
      secondary_info: last-changed
      card_mod:
        style:
          hui-generic-entity-row:
            $: |
              state-badge {
                filter: {{(states(config.entity) == 'not_home')|iif('grayscale(1)','none')}};
              }
    padding: 0

works for all entities in the fold! never realized we could use more than 1 group_config behavior .

Makes me thinking of the “infamous” Menu Button “History-view” … Load ALL ( set time-range ) Load AL … sure when you do that in a browser on the Server, it takes 2 seconds and everything is there, if you do it on another wifi(n) connected laptop in same network, i stopped counting after 10 :slight_smile:

Thx, hadn’t figured out yet how to “fix” my list off tracked devices … Pay-off for following your "code examples :slight_smile:

Hi Marius, a bit busy because of this @#$@%& war. Cannot spent now many time for HA… Thank you very much for not forgetting me.

@Ildar_Gabdullin Please be safe! Its a horrific situation. Hope you are well.