From 97f62cfb789733693c537d2ad68bbf96260a1c86 Mon Sep 17 00:00:00 2001 From: Teagan Glenn <that@teagantotally.rocks> Date: Thu, 8 Jun 2017 03:26:24 -0600 Subject: [PATCH] [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 --- .../components/image_processing/opencv.py | 156 +++++++++++---- homeassistant/components/opencv.py | 183 ------------------ requirements_all.txt | 5 +- 3 files changed, 124 insertions(+), 220 deletions(-) delete mode 100644 homeassistant/components/opencv.py diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index e6cdd0fcef9..f19d50300b8 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -7,22 +7,56 @@ https://home-assistant.io/components/image_processing.opencv/ from datetime import timedelta import logging +import requests + +import voluptuous as vol + from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - ImageProcessingEntity, PLATFORM_SCHEMA) -from homeassistant.components.opencv import ( - ATTR_MATCHES, CLASSIFIER_GROUP_CONFIG, CONF_CLASSIFIER, CONF_ENTITY_ID, - CONF_NAME, process_image) + CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, PLATFORM_SCHEMA, + ImageProcessingEntity) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['numpy==1.12.0'] _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 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): @@ -37,41 +71,63 @@ def _create_processor_from_config(hass, camera_entity, config): 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): """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 - devices = [] - for camera_entity in discovery_info[CONF_ENTITY_ID]: - devices.append( - _create_processor_from_config(hass, camera_entity, discovery_info)) + entities = [] + if config[CONF_CLASSIFIER] is None: + dest_path = hass.config.path(DEFAULT_CLASSIFIER_PATH) + _get_default_classifier(dest_path) + config[CONF_CLASSIFIER] = { + 'Face': dest_path + } + + for camera in config[CONF_SOURCE]: + entities.append(OpenCVImageProcessor( + hass, camera[CONF_ENTITY_ID], camera.get(CONF_NAME), + config[CONF_CLASSIFIER] + )) - add_devices(devices) + add_devices(entities) class OpenCVImageProcessor(ImageProcessingEntity): """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.""" self.hass = hass self._camera_entity = camera_entity - self._name = name - self._classifier_configs = classifier_configs + if name: + self._name = name + else: + self._name = "OpenCV {0}".format( + split_entity_id(camera_entity)[1]) + self._classifiers = classifiers self._matches = {} + self._total_matches = 0 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 def camera_entity(self): """Return camera entity id from process pictures.""" @@ -85,20 +141,54 @@ class OpenCVImageProcessor(ImageProcessingEntity): @property def state(self): """Return the state of the entity.""" - total_matches = 0 - for group in self._matches.values(): - total_matches += len(group) - return total_matches + return self._total_matches @property def state_attributes(self): """Return device specific state attributes.""" return { - ATTR_MATCHES: self._matches + ATTR_MATCHES: self._matches, + ATTR_TOTAL_MATCHES: self._total_matches } def process_image(self, image): """Process the image.""" - self._last_image = image - self._matches = process_image( - image, self._classifier_configs, False) + import cv2 # pylint: disable=import-error + import numpy + + # 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 diff --git a/homeassistant/components/opencv.py b/homeassistant/components/opencv.py deleted file mode 100644 index 634e8e156a1..00000000000 --- a/homeassistant/components/opencv.py +++ /dev/null @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index bf7ad1d9d0d..987779c1155 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,7 +387,7 @@ netdisco==1.0.1 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 -# homeassistant.components.opencv +# homeassistant.components.image_processing.opencv numpy==1.12.0 # homeassistant.components.google @@ -399,9 +399,6 @@ oemthermostat==1.1 # homeassistant.components.media_player.onkyo onkyo-eiscp==1.1 -# homeassistant.components.opencv -# opencv-python==3.2.0.6 - # homeassistant.components.sensor.openevse openevsewifi==0.4 -- GitLab