Skip to content
Merged

Opencv #7261

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
120 changes: 120 additions & 0 deletions homeassistant/components/image_processing/opencv.py
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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please inheret from ImageProcessingFaceEntity

Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor Author

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 ImageProcessingFaceEntity as that would be misleading to what it can process.

Copy link
Copy Markdown
Member

@balloob balloob May 3, 2017

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.

"""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)
182 changes: 182 additions & 0 deletions homeassistant/components/opencv.py
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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kill our loop

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you're right, I missed that.

@Teagan42 we should not do any I/O inside an asynchronous context without yielding. See Async 101

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will address today

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
9 changes: 9 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -412,12 +412,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

Expand Down Expand Up @@ -796,6 +802,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

Expand Down