Created
February 11, 2014 20:36
-
-
Save cees-elzinga/8943531 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# | |
# Ambilight for XBMC picture slideshow | |
# | |
# proof-of-concept | |
# | |
# Depends on: | |
# - Python Image library | |
# - XBMC HTTP API enabled in System -> Services -> Webserver | |
# - Allow Control of XBMC via HTTP enabled | |
import Image | |
import requests | |
import json | |
import time | |
import colorsys | |
# XBMC HTTP API settings | |
# Copy from System -> Services -> Webserver | |
HOST = "localhost" | |
PORT = "8080" | |
AUTH = ("username", "password") | |
# Philips Hue Bridge settings | |
IP = "192.168.1.1" | |
KEY = "abcdef0123456789abcdef0123456789" | |
LIGHT = 2 | |
# Global settings | |
SIZE = (32, 32) | |
fmtRGBA = True | |
class Settings(): | |
color_bias = 36 | |
settings = Settings() | |
# FIXME: Test executeJSONRPC function | |
# The Python transport can only be used by XBMC addons through the executeJSONRPC | |
# method provided by the xbmc python library. As it must be available to every addon | |
# in an XBMC installation it must not be enabled or disabled by the user. | |
r = requests.get("http://{}:{}/jsonrpc".format(HOST, PORT), auth=AUTH) | |
if not r.ok: raise Exception("Could not connect") | |
def rpc(data): | |
headers = {"Content-Type": "application/json"} | |
return requests.post( | |
"http://{}:{}/jsonrpc".format(HOST, PORT), | |
data=json.dumps(data), | |
auth=AUTH, | |
headers=headers, | |
) | |
def get_slideshow_player(): | |
r = rpc({"jsonrpc": "2.0", "method": "Player.GetActivePlayers", "id": 1}) | |
if not r.ok: | |
return False | |
r_json = json.loads(r.text) | |
if len(r_json['result']) == 0: | |
return False | |
if r_json['result'][0]['type'] != "picture": | |
return False | |
return r_json['result'][0]['playerid'] | |
def get_slideshow_picture(): | |
r = rpc({"jsonrpc": "2.0", | |
"method": "Player.GetItem", | |
"params": { | |
"properties": ["file"], | |
"playerid": player_id | |
}, | |
"id": "PictureGetItem", | |
}) | |
r_json = json.loads(r.text) | |
return r_json['result']['item']['file'] | |
def get_pixels(filename): | |
# convert PixelArray to flat list of RGBARGBA etc | |
all_pixels = [] | |
im = Image.open(filename) | |
pixels = im.load() | |
im.thumbnail(SIZE, Image.ANTIALIAS) | |
for x in range(SIZE[0]): | |
for y in range(SIZE[1]): | |
# Assume RGB | |
all_pixels.append(pixels[x, y][0]) | |
all_pixels.append(pixels[x, y][1]) | |
all_pixels.append(pixels[x, y][2]) | |
# requires an alpha value (but it's not used), set is to zero | |
all_pixels.append(0) | |
return all_pixels | |
def set_light(hue, sat, bri, dur=20): | |
data = json.dumps({ | |
"on": True, | |
"hue": hue, | |
"sat": sat, | |
"bri": bri, | |
"transitiontime": dur | |
}) | |
requests.put("http://%s/api/%s/lights/%s/state" % \ | |
(IP, KEY, LIGHT), data=data) | |
# | |
# | |
# Code re-used from script.xbmc.ambilight.hue add-on | |
# | |
# | |
class Screenshot: | |
def __init__(self, pixels, capture_width, capture_height): | |
self.pixels = pixels | |
self.capture_width = capture_width | |
self.capture_height = capture_height | |
def most_used_spectrum(self, spectrum, saturation, value, size, overall_value): | |
# color bias/groups 6 - 36 in steps of 3 | |
colorGroups = settings.color_bias | |
if colorGroups == 0: | |
colorGroups = 1 | |
colorHueRatio = 360 / colorGroups | |
hsvRatios = [] | |
hsvRatiosDict = {} | |
for i in range(360): | |
if spectrum.has_key(i): | |
#shift index to the right so that groups are centered on primary and secondary colors | |
colorIndex = int(((i+colorHueRatio/2) % 360)/colorHueRatio) | |
pixelCount = spectrum[i] | |
if hsvRatiosDict.has_key(colorIndex): | |
hsvr = hsvRatiosDict[colorIndex] | |
hsvr.average(i/360.0, saturation[i], value[i]) | |
hsvr.ratio = hsvr.ratio + pixelCount / float(size) | |
else: | |
hsvr = HSVRatio(i/360.0, saturation[i], value[i], pixelCount / float(size)) | |
hsvRatiosDict[colorIndex] = hsvr | |
hsvRatios.append(hsvr) | |
colorCount = len(hsvRatios) | |
if colorCount > 1: | |
# sort colors by popularity | |
hsvRatios = sorted(hsvRatios, key=lambda hsvratio: hsvratio.ratio, reverse=True) | |
# logger.debuglog("hsvRatios %s" % hsvRatios) | |
#return at least 3 | |
if colorCount == 2: | |
hsvRatios.insert(0, hsvRatios[0]) | |
hsvRatios[0].averageValue(overall_value) | |
hsvRatios[1].averageValue(overall_value) | |
hsvRatios[2].averageValue(overall_value) | |
return hsvRatios | |
elif colorCount == 1: | |
hsvRatios[0].averageValue(overall_value) | |
return [hsvRatios[0]] * 3 | |
else: | |
return [HSVRatio()] * 3 | |
def spectrum_hsv(self, pixels, width, height): | |
spectrum = {} | |
saturation = {} | |
value = {} | |
size = int(len(pixels)/4) | |
pixel = 0 | |
i = 0 | |
s, v = 0, 0 | |
r, g, b = 0, 0, 0 | |
tmph, tmps, tmpv = 0, 0, 0 | |
for i in range(size): | |
if fmtRGBA: | |
r = pixels[pixel] | |
g = pixels[pixel + 1] | |
b = pixels[pixel + 2] | |
else: #probably BGRA | |
b = pixels[pixel] | |
g = pixels[pixel + 1] | |
r = pixels[pixel + 2] | |
pixel += 4 | |
tmph, tmps, tmpv = colorsys.rgb_to_hsv(float(r/255.0), float(g/255.0), float(b/255.0)) | |
s += tmps | |
v += tmpv | |
# skip low value and saturation | |
if tmpv > 0.25: | |
if tmps > 0.33: | |
h = int(tmph * 360) | |
# logger.debuglog("%s \t set pixel r %s \tg %s \tb %s" % (i, r, g, b)) | |
# logger.debuglog("%s \t set pixel h %s \ts %s \tv %s" % (i, tmph*100, tmps*100, tmpv*100)) | |
if spectrum.has_key(h): | |
spectrum[h] += 1 # tmps * 2 * tmpv | |
saturation[h] = (saturation[h] + tmps)/2 | |
value[h] = (value[h] + tmpv)/2 | |
else: | |
spectrum[h] = 1 # tmps * 2 * tmpv | |
saturation[h] = tmps | |
value[h] = tmpv | |
overall_value = v / float(i) | |
# s_overall = int(s * 100 / i) | |
return self.most_used_spectrum(spectrum, saturation, value, size, overall_value) | |
class HSVRatio: | |
cyan_min = float(4.5/12.0) | |
cyan_max = float(7.75/12.0) | |
def __init__(self, hue=0.0, saturation=0.0, value=0.0, ratio=0.0): | |
self.h = hue | |
self.s = saturation | |
self.v = value | |
self.ratio = ratio | |
def average(self, h, s, v): | |
self.h = (self.h + h)/2 | |
self.s = (self.s + s)/2 | |
self.v = (self.v + v)/2 | |
def averageValue(self, overall_value): | |
if self.ratio > 0.5: | |
self.v = self.v * self.ratio + overall_value * (1-self.ratio) | |
else: | |
self.v = (self.v + overall_value)/2 | |
def hue(self, fullSpectrum): | |
if fullSpectrum != True: | |
if self.s > 0.01: | |
if self.h < 0.5: | |
#yellow-green correction | |
self.h = self.h * 1.17 | |
#cyan-green correction | |
if self.h > self.cyan_min: | |
self.h = self.cyan_min | |
else: | |
#cyan-blue correction | |
if self.h < self.cyan_max: | |
self.h = self.cyan_max | |
h = int(self.h*65535) # on a scale from 0 <-> 65535 | |
s = int(self.s*255) | |
v = int(self.v*255) | |
# Disabled for this script | |
#if v < hue.settings.ambilight_min: | |
# v = hue.settings.ambilight_min | |
#if v > hue.settings.ambilight_max: | |
# v = hue.settings.ambilight_max | |
return h, s, v | |
def __repr__(self): | |
return 'h: %s s: %s v: %s ratio: %s' % (self.h, self.s, self.v, self.ratio) | |
# | |
# | |
# End code re-used from script.xbmc.ambilight.hue add-on | |
# | |
# | |
last = None | |
while True: | |
player_id = get_slideshow_player() | |
if not player_id: | |
print "[*] No slideshow running, waiting 3s" | |
time.sleep(3) | |
continue | |
time.sleep(1.0/20) | |
picture = get_slideshow_picture() | |
if last == picture: | |
continue | |
print "[*] New picture" | |
last = picture | |
screen = Screenshot(get_pixels(picture), SIZE[0], SIZE[1]) | |
hsvRatios = screen.spectrum_hsv(screen.pixels, screen.capture_width, screen.capture_height) | |
h, s, v = hsvRatios[0].hue(True) | |
set_light(h, s, v, 10) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment