From b321e0ef80b2e7ae29fcec6580d662a98d4457e3 Mon Sep 17 00:00:00 2001 From: Teagan Glenn <that@teagantotally.rocks> Date: Tue, 2 May 2017 22:55:51 -0600 Subject: [PATCH] Opencv (#7261) * OpenCV * Fix * Type-o * Remove unused opencv camera component discovery. --- .coveragerc | 3 + .../components/image_processing/opencv.py | 120 ++++++++++++ homeassistant/components/opencv.py | 182 ++++++++++++++++++ requirements_all.txt | 9 + 4 files changed, 314 insertions(+) create mode 100644 homeassistant/components/image_processing/opencv.py create mode 100644 homeassistant/components/opencv.py diff --git a/.coveragerc b/.coveragerc index ed969e54511..279dc114a21 100644 --- a/.coveragerc +++ b/.coveragerc @@ -77,6 +77,9 @@ omit = homeassistant/components/octoprint.py homeassistant/components/*/octoprint.py + homeassistant/components/opencv.py + homeassistant/components/*/opencv.py + homeassistant/components/qwikswitch.py homeassistant/components/*/qwikswitch.py diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py new file mode 100644 index 00000000000..e48c14aeea5 --- /dev/null +++ b/homeassistant/components/image_processing/opencv.py @@ -0,0 +1,120 @@ +""" +Component that performs OpenCV classification on images. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/image_processing.opencv/ +""" +from datetime import timedelta +import logging + +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, +) + +DEPENDENCIES = ['opencv'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_TIMEOUT = 10 + +SCAN_INTERVAL = timedelta(seconds=2) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(CLASSIFIER_GROUP_CONFIG) + + +def _create_processor_from_config(hass, camera_entity, config): + """Create an OpenCV processor from configurtaion.""" + classifier_config = config[CONF_CLASSIFIER] + name = '{} {}'.format( + config[CONF_NAME], + split_entity_id(camera_entity)[1].replace('_', ' ')) + + processor = OpenCVImageProcessor( + hass, + camera_entity, + name, + classifier_config, + ) + + return processor + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the OpenCV image processing platform.""" + if discovery_info is None: + return + + devices = [] + for camera_entity in discovery_info[CONF_ENTITY_ID]: + devices.append( + _create_processor_from_config( + hass, + camera_entity, + discovery_info)) + + add_devices(devices) + + +class OpenCVImageProcessor(ImageProcessingEntity): + """Representation of an OpenCV image processor.""" + + def __init__(self, hass, camera_entity, name, classifier_configs): + """Initialize the OpenCV entity.""" + self.hass = hass + self._camera_entity = camera_entity + self._name = name + self._classifier_configs = classifier_configs + self._matches = {} + 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.""" + return self._camera_entity + + @property + def name(self): + """Return the name of the image processor.""" + return self._name + + @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 + + @property + def state_attributes(self): + """Return device specific state attributes.""" + return { + ATTR_MATCHES: self._matches + } + + def process_image(self, image): + """Process the image.""" + self._last_image = image + self._matches = process_image(image, + self._classifier_configs, + False) diff --git a/homeassistant/components/opencv.py b/homeassistant/components/opencv.py new file mode 100644 index 00000000000..9f53f0841a1 --- /dev/null +++ b/homeassistant/components/opencv.py @@ -0,0 +1,182 @@ +""" +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 asyncio +import logging +import os +import voluptuous as vol + +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', 'urllib3==1.21'] + +_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 = os.path.join( + os.path.dirname(BASE_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=DEFAULT_CLASSIFIER_PATH): + 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=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 + 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 + 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 + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the OpenCV platform entities.""" + _LOGGER.info('Async setup for opencv') + if not os.path.isfile(DEFAULT_CLASSIFIER_PATH): + _LOGGER.info('Downloading default classifier') + import urllib3 + + http = urllib3.PoolManager() + request = http.request('GET', CASCADE_URL, preload_content=False) + + with open(DEFAULT_CLASSIFIER_PATH, 'wb') as out: + while True: + data = request.read(1028) + if not data: + break + out.write(data) + + request.release_conn() + + for group in config[DOMAIN][CONF_GROUPS]: + discovery.load_platform(hass, 'image_processing', DOMAIN, group) + + return True diff --git a/requirements_all.txt b/requirements_all.txt index 64f6500638e..2208fde8f18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,12 +408,18 @@ netdisco==1.0.0rc3 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 +# homeassistant.components.opencv +numpy==1.12.0 + # homeassistant.components.google oauth2client==4.0.0 # homeassistant.components.climate.oem oemthermostat==1.1 +# homeassistant.components.opencv +opencv-python==3.2.0.6 + # homeassistant.components.sensor.openevse openevsewifi==0.4 @@ -802,6 +808,9 @@ uber_rides==0.4.1 # homeassistant.components.sensor.ups upsmychoice==1.0.2 +# homeassistant.components.opencv +urllib3==1.21 + # homeassistant.components.camera.uvc uvcclient==0.10.0 -- GitLab