Skip to content

Commit 3eaf092

Browse files
authored
Merge pull request #1 from relvacode/image-downsample
Faster palette extraction through image downsampling
2 parents 13ffb5e + d812a90 commit 3eaf092

4 files changed

Lines changed: 63 additions & 13 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,14 @@ media_player:
4848
- platform: lightcast
4949
name: Office LightCast
5050
target: area.office
51+
```
52+
53+
It can take a while to extract the palette of very large images. Use `downsample` to downsample images to a width of `256`. The default is true.
54+
55+
```yaml
56+
media_player:
57+
- platform: lightcast
58+
name: Living Room LightCast
59+
target: area.living_room
60+
# downsample: false
5161
```
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import io
2+
import typing
3+
import itertools
4+
5+
import colorgram
6+
from PIL import Image
7+
8+
RGB = typing.Tuple[float, float, float]
9+
10+
MAX_SAMPLE_SIZE = 256
11+
12+
13+
def extract_palette(f: io.BytesIO | Image.Image, colors: int, downsample: bool = True) -> list[RGB]:
14+
image = f if isinstance(f, Image.Image) else Image.open(f)
15+
if image.mode not in ('RGB', 'RGBA', 'RGBa'):
16+
image = image.convert('RGB')
17+
18+
# Downsample image
19+
if downsample and (image.width > MAX_SAMPLE_SIZE or image.height > MAX_SAMPLE_SIZE):
20+
aspect = MAX_SAMPLE_SIZE / float(image.width)
21+
height = int(float(image.height) * aspect)
22+
image = image.resize((MAX_SAMPLE_SIZE, height), resample=Image.Resampling.NEAREST)
23+
24+
extracted_colors = colorgram.extract(image, colors)
25+
26+
palette = []
27+
for i, color in zip(range(colors), itertools.cycle(extracted_colors)):
28+
palette.append((color.rgb.r, color.rgb.g, color.rgb.b))
29+
30+
return palette
31+
32+
33+
if __name__ == "__main__":
34+
def main():
35+
import sys
36+
37+
with open(sys.argv[1], 'rb') as f:
38+
palette = extract_palette(f, int(sys.argv[2]))
39+
for color in palette:
40+
print(color)
41+
42+
43+
main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
CONF_TARGET = 'target'
22
CONF_NAME = 'name'
33
CONF_FILTER_ON = 'filter_on'
4+
CONF_DOWNSAMPLE = 'downsample'

custom_components/lightcast/media_player.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import logging
22
import io
3-
import itertools
43
import aiohttp
5-
import colorgram
64

75
from homeassistant.core import HomeAssistant
86
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -19,6 +17,7 @@
1917

2018
from . import const
2119
from .entity_resolver import expand_entities
20+
from .color_extract import extract_palette
2221

2322
_LOGGER = logging.getLogger(__name__)
2423

@@ -35,7 +34,8 @@ async def async_setup_platform(
3534
hass,
3635
config[const.CONF_NAME],
3736
config[const.CONF_TARGET],
38-
config.get(const.CONF_FILTER_ON) or False
37+
config.get(const.CONF_FILTER_ON) or False,
38+
True if (downsample := config.get(const.CONF_DOWNSAMPLE)) is None else downsample,
3939
)
4040

4141
add_entities([cast_device])
@@ -46,11 +46,12 @@ class LightCastPlayer(MediaPlayerEntity):
4646
_attr_supported_features = MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.BROWSE_MEDIA
4747
_attr_device_class = MediaPlayerDeviceClass.TV
4848

49-
def __init__(self, hass: HomeAssistant, name: str, device_target: str, filter_on: bool) -> None:
49+
def __init__(self, hass: HomeAssistant, name: str, device_target: str, filter_on: bool, downsample: bool) -> None:
5050
self.hass = hass
5151
self._attr_name = name
5252
self.device_target = device_target
5353
self.filter_on = filter_on
54+
self.downsample = downsample
5455

5556
async def async_browse_media(self, media_content_type: str | None = None,
5657
media_content_id: str | None = None) -> BrowseMedia:
@@ -95,21 +96,16 @@ async def process_image(self, media_type: str, media_id: str) -> None:
9596
) as response:
9697
response_data = await response.read()
9798

98-
n_colors = len(valid_entities)
99-
_LOGGER.info('Downloaded image, extracting palette of %d colors', n_colors)
99+
palette = extract_palette(io.BytesIO(response_data), len(valid_entities), downsample=self.downsample)
100100

101-
extracted_colors = colorgram.extract(io.BytesIO(response_data), n_colors)
102-
_LOGGER.info('Extracted palette of %d colors from image', len(extracted_colors))
103-
104-
for e, color in zip(valid_entities, itertools.cycle(extracted_colors)):
105-
rgb_color = [color.rgb.r, color.rgb.g, color.rgb.b]
106-
_LOGGER.info('Set light %s to %s', e.entity_id, rgb_color)
101+
for e, color in zip(valid_entities, palette):
102+
_LOGGER.info('Set light %s to %s', e.entity_id, color)
107103
await self.hass.services.async_call(
108104
light.DOMAIN,
109105
light.SERVICE_TURN_ON,
110106
{
111107
ATTR_ENTITY_ID: e.entity_id,
112-
light.ATTR_RGB_COLOR: rgb_color
108+
light.ATTR_RGB_COLOR: color
113109
}
114110
)
115111

0 commit comments

Comments
 (0)