-
-
Notifications
You must be signed in to change notification settings - Fork 37.6k
Opencv #7261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Opencv #7261
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use only setup. Or the fetch will lock all |
||
| """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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kill our loop
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will address today
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I already did #7419 |
||
| 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please inheret from
ImageProcessingFaceEntityThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it's not necessarily a Face - just the default classifier is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The entity could be anything - if you had a car classifier, it would be a car, not a face, if it was a tree classifier, it's a tree
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are classifiers that people can use (without even getting into making their own):
https://github.com/opencv/opencv/tree/master/data/haarcascades
https://github.com/opencv/opencv/tree/master/data/lbpcascades
Examples include:
Cat, Cat Face, Silverware, Upper Body, Lower Body, Full Body, License Plates
I do not agree with making this a
ImageProcessingFaceEntityas that would be misleading to what it can process.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the Face entity could actually be easily changed to be a generic Thing detector with a confidence level etc. In fact, that is pretty much what @Teagan42 has here. I think it's fine as is. The important part here is that the base entity from the component is filled in.