mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 17:57:11 +00:00
[WIP] Fix opencv (#7864)
* Updates to opencv image processor * Remove opencv hub * Requirements * Remove extra line * Fix linting errors * Indentation * Requirements * Linting * Check for import on platform setup * Remove opencv requirement * Linting * fix style * fix lint
This commit is contained in:
parent
482db94372
commit
97f62cfb78
@ -7,22 +7,56 @@ https://home-assistant.io/components/image_processing.opencv/
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import split_entity_id
|
from homeassistant.core import split_entity_id
|
||||||
from homeassistant.components.image_processing import (
|
from homeassistant.components.image_processing import (
|
||||||
ImageProcessingEntity, PLATFORM_SCHEMA)
|
CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, PLATFORM_SCHEMA,
|
||||||
from homeassistant.components.opencv import (
|
ImageProcessingEntity)
|
||||||
ATTR_MATCHES, CLASSIFIER_GROUP_CONFIG, CONF_CLASSIFIER, CONF_ENTITY_ID,
|
import homeassistant.helpers.config_validation as cv
|
||||||
CONF_NAME, process_image)
|
|
||||||
|
REQUIREMENTS = ['numpy==1.12.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['opencv']
|
ATTR_MATCHES = 'matches'
|
||||||
|
ATTR_TOTAL_MATCHES = 'total_matches'
|
||||||
|
|
||||||
|
CASCADE_URL = \
|
||||||
|
'https://raw.githubusercontent.com/opencv/opencv/master/data/' + \
|
||||||
|
'lbpcascades/lbpcascade_frontalface.xml'
|
||||||
|
|
||||||
|
CONF_CLASSIFIER = 'classifer'
|
||||||
|
CONF_FILE = 'file'
|
||||||
|
CONF_MIN_SIZE = 'min_size'
|
||||||
|
CONF_NEIGHBORS = 'neighbors'
|
||||||
|
CONF_SCALE = 'scale'
|
||||||
|
|
||||||
|
DEFAULT_CLASSIFIER_PATH = 'lbp_frontalface.xml'
|
||||||
|
DEFAULT_MIN_SIZE = (30, 30)
|
||||||
|
DEFAULT_NEIGHBORS = 4
|
||||||
|
DEFAULT_SCALE = 1.1
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=2)
|
SCAN_INTERVAL = timedelta(seconds=2)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(CLASSIFIER_GROUP_CONFIG)
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_CLASSIFIER, default=None): {
|
||||||
|
cv.string: vol.Any(
|
||||||
|
cv.isfile,
|
||||||
|
vol.Schema({
|
||||||
|
vol.Required(CONF_FILE): cv.isfile,
|
||||||
|
vol.Optional(CONF_SCALE, DEFAULT_SCALE): float,
|
||||||
|
vol.Optional(CONF_NEIGHBORS, DEFAULT_NEIGHBORS):
|
||||||
|
cv.positive_int,
|
||||||
|
vol.Optional(CONF_MIN_SIZE, DEFAULT_MIN_SIZE):
|
||||||
|
vol.Schema((int, int))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def _create_processor_from_config(hass, camera_entity, config):
|
def _create_processor_from_config(hass, camera_entity, config):
|
||||||
@ -37,41 +71,63 @@ def _create_processor_from_config(hass, camera_entity, config):
|
|||||||
return processor
|
return processor
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_classifier(dest_path):
|
||||||
|
"""Download the default OpenCV classifier."""
|
||||||
|
_LOGGER.info('Downloading default classifier')
|
||||||
|
req = requests.get(CASCADE_URL, stream=True)
|
||||||
|
with open(dest_path, 'wb') as fil:
|
||||||
|
for chunk in req.iter_content(chunk_size=1024):
|
||||||
|
if chunk: # filter out keep-alive new chunks
|
||||||
|
fil.write(chunk)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the OpenCV image processing platform."""
|
"""Set up the OpenCV image processing platform."""
|
||||||
if discovery_info is None:
|
try:
|
||||||
|
# Verify opencv python package is preinstalled
|
||||||
|
# pylint: disable=unused-import,unused-variable
|
||||||
|
import cv2 # noqa
|
||||||
|
except ImportError:
|
||||||
|
_LOGGER.error("No opencv library found! " +
|
||||||
|
"Install or compile for your system " +
|
||||||
|
"following instructions here: " +
|
||||||
|
"http://opencv.org/releases.html")
|
||||||
return
|
return
|
||||||
|
|
||||||
devices = []
|
entities = []
|
||||||
for camera_entity in discovery_info[CONF_ENTITY_ID]:
|
if config[CONF_CLASSIFIER] is None:
|
||||||
devices.append(
|
dest_path = hass.config.path(DEFAULT_CLASSIFIER_PATH)
|
||||||
_create_processor_from_config(hass, camera_entity, discovery_info))
|
_get_default_classifier(dest_path)
|
||||||
|
config[CONF_CLASSIFIER] = {
|
||||||
|
'Face': dest_path
|
||||||
|
}
|
||||||
|
|
||||||
add_devices(devices)
|
for camera in config[CONF_SOURCE]:
|
||||||
|
entities.append(OpenCVImageProcessor(
|
||||||
|
hass, camera[CONF_ENTITY_ID], camera.get(CONF_NAME),
|
||||||
|
config[CONF_CLASSIFIER]
|
||||||
|
))
|
||||||
|
|
||||||
|
add_devices(entities)
|
||||||
|
|
||||||
|
|
||||||
class OpenCVImageProcessor(ImageProcessingEntity):
|
class OpenCVImageProcessor(ImageProcessingEntity):
|
||||||
"""Representation of an OpenCV image processor."""
|
"""Representation of an OpenCV image processor."""
|
||||||
|
|
||||||
def __init__(self, hass, camera_entity, name, classifier_configs):
|
def __init__(self, hass, camera_entity, name, classifiers):
|
||||||
"""Initialize the OpenCV entity."""
|
"""Initialize the OpenCV entity."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._camera_entity = camera_entity
|
self._camera_entity = camera_entity
|
||||||
self._name = name
|
if name:
|
||||||
self._classifier_configs = classifier_configs
|
self._name = name
|
||||||
|
else:
|
||||||
|
self._name = "OpenCV {0}".format(
|
||||||
|
split_entity_id(camera_entity)[1])
|
||||||
|
self._classifiers = classifiers
|
||||||
self._matches = {}
|
self._matches = {}
|
||||||
|
self._total_matches = 0
|
||||||
self._last_image = None
|
self._last_image = None
|
||||||
|
|
||||||
@property
|
|
||||||
def last_image(self):
|
|
||||||
"""Return the last image."""
|
|
||||||
return self._last_image
|
|
||||||
|
|
||||||
@property
|
|
||||||
def matches(self):
|
|
||||||
"""Return the matches it found."""
|
|
||||||
return self._matches
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def camera_entity(self):
|
def camera_entity(self):
|
||||||
"""Return camera entity id from process pictures."""
|
"""Return camera entity id from process pictures."""
|
||||||
@ -85,20 +141,54 @@ class OpenCVImageProcessor(ImageProcessingEntity):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the entity."""
|
"""Return the state of the entity."""
|
||||||
total_matches = 0
|
return self._total_matches
|
||||||
for group in self._matches.values():
|
|
||||||
total_matches += len(group)
|
|
||||||
return total_matches
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return device specific state attributes."""
|
"""Return device specific state attributes."""
|
||||||
return {
|
return {
|
||||||
ATTR_MATCHES: self._matches
|
ATTR_MATCHES: self._matches,
|
||||||
|
ATTR_TOTAL_MATCHES: self._total_matches
|
||||||
}
|
}
|
||||||
|
|
||||||
def process_image(self, image):
|
def process_image(self, image):
|
||||||
"""Process the image."""
|
"""Process the image."""
|
||||||
self._last_image = image
|
import cv2 # pylint: disable=import-error
|
||||||
self._matches = process_image(
|
import numpy
|
||||||
image, self._classifier_configs, False)
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
cv_image = cv2.imdecode(numpy.asarray(bytearray(image)),
|
||||||
|
cv2.IMREAD_UNCHANGED)
|
||||||
|
|
||||||
|
for name, classifier in self._classifiers.items():
|
||||||
|
scale = DEFAULT_SCALE
|
||||||
|
neighbors = DEFAULT_NEIGHBORS
|
||||||
|
min_size = DEFAULT_MIN_SIZE
|
||||||
|
if isinstance(classifier, dict):
|
||||||
|
path = classifier[CONF_FILE]
|
||||||
|
scale = classifier.get(CONF_SCALE, scale)
|
||||||
|
neighbors = classifier.get(CONF_NEIGHBORS, neighbors)
|
||||||
|
min_size = classifier.get(CONF_MIN_SIZE, min_size)
|
||||||
|
else:
|
||||||
|
path = classifier
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
cascade = cv2.CascadeClassifier(path)
|
||||||
|
|
||||||
|
detections = cascade.detectMultiScale(
|
||||||
|
cv_image,
|
||||||
|
scaleFactor=scale,
|
||||||
|
minNeighbors=neighbors,
|
||||||
|
minSize=min_size)
|
||||||
|
matches = {}
|
||||||
|
total_matches = 0
|
||||||
|
regions = []
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
for (x, y, w, h) in detections:
|
||||||
|
regions.append((int(x), int(y), int(w), int(h)))
|
||||||
|
total_matches += 1
|
||||||
|
|
||||||
|
matches[name] = regions
|
||||||
|
|
||||||
|
self._matches = matches
|
||||||
|
self._total_matches = total_matches
|
||||||
|
@ -1,183 +0,0 @@
|
|||||||
"""
|
|
||||||
Support for OpenCV image/video processing.
|
|
||||||
|
|
||||||
For more details about this component, please refer to the documentation at
|
|
||||||
https://home-assistant.io/components/opencv/
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONF_NAME,
|
|
||||||
CONF_ENTITY_ID,
|
|
||||||
CONF_FILE_PATH
|
|
||||||
)
|
|
||||||
from homeassistant.helpers import (
|
|
||||||
discovery,
|
|
||||||
config_validation as cv,
|
|
||||||
)
|
|
||||||
|
|
||||||
REQUIREMENTS = ['opencv-python==3.2.0.6', 'numpy==1.12.0']
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ATTR_MATCHES = 'matches'
|
|
||||||
|
|
||||||
BASE_PATH = os.path.realpath(__file__)
|
|
||||||
|
|
||||||
CASCADE_URL = \
|
|
||||||
'https://raw.githubusercontent.com/opencv/opencv/master/data/' +\
|
|
||||||
'lbpcascades/lbpcascade_frontalface.xml'
|
|
||||||
|
|
||||||
CONF_CLASSIFIER = 'classifier'
|
|
||||||
CONF_COLOR = 'color'
|
|
||||||
CONF_GROUPS = 'classifier_group'
|
|
||||||
CONF_MIN_SIZE = 'min_size'
|
|
||||||
CONF_NEIGHBORS = 'neighbors'
|
|
||||||
CONF_SCALE = 'scale'
|
|
||||||
|
|
||||||
DATA_CLASSIFIER_GROUPS = 'classifier_groups'
|
|
||||||
|
|
||||||
DEFAULT_COLOR = (255, 255, 0)
|
|
||||||
DEFAULT_CLASSIFIER_PATH = 'lbp_frontalface.xml'
|
|
||||||
DEFAULT_NAME = 'OpenCV'
|
|
||||||
DEFAULT_MIN_SIZE = (30, 30)
|
|
||||||
DEFAULT_NEIGHBORS = 4
|
|
||||||
DEFAULT_SCALE = 1.1
|
|
||||||
|
|
||||||
DOMAIN = 'opencv'
|
|
||||||
|
|
||||||
CLASSIFIER_GROUP_CONFIG = {
|
|
||||||
vol.Required(CONF_CLASSIFIER): vol.All(
|
|
||||||
cv.ensure_list,
|
|
||||||
[vol.Schema({
|
|
||||||
vol.Optional(CONF_COLOR, default=DEFAULT_COLOR):
|
|
||||||
vol.Schema((int, int, int)),
|
|
||||||
vol.Optional(CONF_FILE_PATH, default=None): cv.isfile,
|
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME):
|
|
||||||
cv.string,
|
|
||||||
vol.Optional(CONF_MIN_SIZE, default=DEFAULT_MIN_SIZE):
|
|
||||||
vol.Schema((int, int)),
|
|
||||||
vol.Optional(CONF_NEIGHBORS, default=DEFAULT_NEIGHBORS):
|
|
||||||
cv.positive_int,
|
|
||||||
vol.Optional(CONF_SCALE, default=DEFAULT_SCALE):
|
|
||||||
float
|
|
||||||
})]),
|
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
||||||
}
|
|
||||||
CLASSIFIER_GROUP_SCHEMA = vol.Schema(CLASSIFIER_GROUP_CONFIG)
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
|
||||||
DOMAIN: vol.Schema({
|
|
||||||
vol.Required(CONF_GROUPS): vol.All(
|
|
||||||
cv.ensure_list,
|
|
||||||
[CLASSIFIER_GROUP_SCHEMA]
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE:
|
|
||||||
# pylint cannot find any of the members of cv2, using disable=no-member
|
|
||||||
# to pass linting
|
|
||||||
|
|
||||||
|
|
||||||
def cv_image_to_bytes(cv_image):
|
|
||||||
"""Convert OpenCV image to bytes."""
|
|
||||||
import cv2 # pylint: disable=import-error
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
|
||||||
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 90]
|
|
||||||
# pylint: disable=no-member
|
|
||||||
success, data = cv2.imencode('.jpg', cv_image, encode_param)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
return data.tobytes()
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def cv_image_from_bytes(image):
|
|
||||||
"""Convert image bytes to OpenCV image."""
|
|
||||||
import cv2 # pylint: disable=import-error
|
|
||||||
import numpy
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
|
||||||
return cv2.imdecode(numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED)
|
|
||||||
|
|
||||||
|
|
||||||
def process_image(image, classifier_group, is_camera):
|
|
||||||
"""Process the image given a classifier group."""
|
|
||||||
import cv2 # pylint: disable=import-error
|
|
||||||
import numpy
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
|
||||||
cv_image = cv2.imdecode(numpy.asarray(bytearray(image)),
|
|
||||||
cv2.IMREAD_UNCHANGED)
|
|
||||||
group_matches = {}
|
|
||||||
for classifier_config in classifier_group:
|
|
||||||
classifier_path = classifier_config[CONF_FILE_PATH]
|
|
||||||
classifier_name = classifier_config[CONF_NAME]
|
|
||||||
color = classifier_config[CONF_COLOR]
|
|
||||||
scale = classifier_config[CONF_SCALE]
|
|
||||||
neighbors = classifier_config[CONF_NEIGHBORS]
|
|
||||||
min_size = classifier_config[CONF_MIN_SIZE]
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
|
||||||
classifier = cv2.CascadeClassifier(classifier_path)
|
|
||||||
|
|
||||||
detections = classifier.detectMultiScale(cv_image,
|
|
||||||
scaleFactor=scale,
|
|
||||||
minNeighbors=neighbors,
|
|
||||||
minSize=min_size)
|
|
||||||
regions = []
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
for (x, y, w, h) in detections:
|
|
||||||
if is_camera:
|
|
||||||
# pylint: disable=no-member
|
|
||||||
cv2.rectangle(cv_image,
|
|
||||||
(x, y),
|
|
||||||
(x + w, y + h),
|
|
||||||
color,
|
|
||||||
2)
|
|
||||||
else:
|
|
||||||
regions.append((int(x), int(y), int(w), int(h)))
|
|
||||||
group_matches[classifier_name] = regions
|
|
||||||
|
|
||||||
if is_camera:
|
|
||||||
return cv_image_to_bytes(cv_image)
|
|
||||||
else:
|
|
||||||
return group_matches
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Set up the OpenCV platform entities."""
|
|
||||||
default_classifier = hass.config.path(DEFAULT_CLASSIFIER_PATH)
|
|
||||||
|
|
||||||
if not os.path.isfile(default_classifier):
|
|
||||||
_LOGGER.info('Downloading default classifier')
|
|
||||||
|
|
||||||
req = requests.get(CASCADE_URL, stream=True)
|
|
||||||
with open(default_classifier, 'wb') as fil:
|
|
||||||
for chunk in req.iter_content(chunk_size=1024):
|
|
||||||
if chunk: # filter out keep-alive new chunks
|
|
||||||
fil.write(chunk)
|
|
||||||
|
|
||||||
for group in config[DOMAIN][CONF_GROUPS]:
|
|
||||||
grp = {}
|
|
||||||
|
|
||||||
for classifier, config in group.items():
|
|
||||||
config = dict(config)
|
|
||||||
|
|
||||||
if config[CONF_FILE_PATH] is None:
|
|
||||||
config[CONF_FILE_PATH] = default_classifier
|
|
||||||
|
|
||||||
grp[classifier] = config
|
|
||||||
|
|
||||||
discovery.load_platform(hass, 'image_processing', DOMAIN, grp)
|
|
||||||
|
|
||||||
return True
|
|
@ -387,7 +387,7 @@ netdisco==1.0.1
|
|||||||
# homeassistant.components.sensor.neurio_energy
|
# homeassistant.components.sensor.neurio_energy
|
||||||
neurio==0.3.1
|
neurio==0.3.1
|
||||||
|
|
||||||
# homeassistant.components.opencv
|
# homeassistant.components.image_processing.opencv
|
||||||
numpy==1.12.0
|
numpy==1.12.0
|
||||||
|
|
||||||
# homeassistant.components.google
|
# homeassistant.components.google
|
||||||
@ -399,9 +399,6 @@ oemthermostat==1.1
|
|||||||
# homeassistant.components.media_player.onkyo
|
# homeassistant.components.media_player.onkyo
|
||||||
onkyo-eiscp==1.1
|
onkyo-eiscp==1.1
|
||||||
|
|
||||||
# homeassistant.components.opencv
|
|
||||||
# opencv-python==3.2.0.6
|
|
||||||
|
|
||||||
# homeassistant.components.sensor.openevse
|
# homeassistant.components.sensor.openevse
|
||||||
openevsewifi==0.4
|
openevsewifi==0.4
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user