diff --git a/README.md b/README.md index fb8c74c..9fd93c7 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,16 @@ # deepstack-python Unofficial python API for [DeepStack](https://python.deepstack.cc/). Provides class for making requests to the object detection endpoint, and functions for processing the result. See the Jupyter notebooks for usage. - -## Services -Face and object detection endpoints return bounding boxes of faces and objects respectively. - -TODO: add face registration and recognition. +Run deepstack (CPU, noAVX mode): +``` +docker run -e VISION-DETECTION=True -e VISION-FACE=True -e MODE=High -d \ + -v localstorage:/datastore -p 5000:5000 \ + -e API-KEY="Mysecretkey" \ + --name deepstack deepquestai/deepstack:noavx +``` ## Development * Use `venv` -> `source venv/bin/activate` * `pip install -r requirements-dev.txt` * Run tests with `venv/bin/pytest tests/*` -* Black format with `venv/bin/black deepstack/core.py` +* Black format with `venv/bin/black deepstack/core.py` and `venv/bin/black tests/test_deepstack.py` diff --git a/deepstack/core.py b/deepstack/core.py index 66277d6..e51b0b2 100644 --- a/deepstack/core.py +++ b/deepstack/core.py @@ -12,6 +12,8 @@ ## API urls URL_OBJECT_DETECTION = "http://{}:{}/v1/vision/detection" URL_FACE_DETECTION = "http://{}:{}/v1/vision/face" +URL_FACE_REGISTRATION = "http://{}:{}/v1/vision/face/register" +URL_FACE_RECOGNITION = "http://{}:{}/v1/vision/face/recognize" def format_confidence(confidence: Union[str, float]) -> float: @@ -28,20 +30,35 @@ def get_confidences_above_threshold( return [val for val in confidences if val >= confidence_threshold] -def get_object_labels(predictions: List[Dict]) -> List[str]: +def get_recognised_faces(predictions: List[Dict]) -> List[Dict]: """ - Get a list of the unique object labels predicted. + Get the recognised faces. + """ + try: + matched_faces = { + face["userid"]: round(face["confidence"] * 100, 1) + for face in predictions + if not face["userid"] == "unknown" + } + return matched_faces + except: + return {} + + +def get_objects(predictions: List[Dict]) -> List[str]: + """ + Get a list of the unique objects predicted. """ labels = [pred["label"] for pred in predictions] return list(set(labels)) -def get_label_confidences(predictions: List[Dict], target_label: str): +def get_object_confidences(predictions: List[Dict], target_object: str): """ Return the list of confidences of instances of target label. """ confidences = [ - pred["confidence"] for pred in predictions if pred["label"] == target_label + pred["confidence"] for pred in predictions if pred["label"] == target_object ] return confidences @@ -50,24 +67,29 @@ def get_objects_summary(predictions: List[Dict]): """ Get a summary of the objects detected. """ - labels = get_object_labels(predictions) + objects = get_objects(predictions) return { - label: len(get_label_confidences(predictions, target_label=label)) - for label in labels + target_object: len(get_object_confidences(predictions, target_object)) + for target_object in objects } -def post_image(url: str, image: bytes, api_key: str, timeout: int): +def post_image( + url: str, image_bytes: bytes, api_key: str, timeout: int, data: dict = {} +): """Post an image to Deepstack.""" try: + data["api_key"] = api_key response = requests.post( - url, files={"image": image}, data={"api_key": api_key}, timeout=timeout + url, files={"image": image_bytes}, data=data, timeout=timeout ) return response except requests.exceptions.Timeout: raise DeepstackException( f"Timeout connecting to Deepstack, current timeout is {timeout} seconds" ) + except requests.exceptions.ConnectionError as exc: + raise DeepstackException(f"Connection error: {exc}") class DeepstackException(Exception): @@ -93,13 +115,8 @@ def __init__( self._timeout = timeout self._predictions = [] - def process_file(self, file_path: str): - """Process an image file.""" - with open(file_path, "rb") as image_bytes: - self.process_image_bytes(image_bytes) - - def process_image_bytes(self, image_bytes: bytes): - """Process an image.""" + def detect(self, image_bytes: bytes): + """Process image_bytes, performing detection.""" self._predictions = [] url = self._url_detection.format(self._ip_address, self._port) @@ -132,6 +149,7 @@ def __init__( ip_address, port, api_key, timeout, url_detection=URL_OBJECT_DETECTION ) + class DeepstackFace(Deepstack): """Work with objects""" @@ -145,3 +163,36 @@ def __init__( super().__init__( ip_address, port, api_key, timeout, url_detection=URL_FACE_DETECTION ) + + def register_face(self, name: str, image_bytes: bytes): + """ + Register a face name to a file. + """ + + response = post_image( + url=URL_FACE_REGISTRATION.format(self._ip_address, self._port), + image_bytes=image_bytes, + api_key=self._api_key, + timeout=self._timeout, + data={"userid": name}, + ) + + if response.status_code == 200 and response.json()["success"] == True: + return + elif response.status_code == 200 and response.json()["success"] == False: + error = response.json()["error"] + raise DeepstackException(f"Error from Deepstack: {error}") + + def recognise(self, image_bytes: bytes): + """Process image_bytes, performing recognition.""" + self._predictions = [] + url = URL_FACE_RECOGNITION.format(self._ip_address, self._port) + + response = post_image(url, image_bytes, self._api_key, self._timeout) + + if response.status_code == HTTP_OK: + if response.json()["success"]: + self._predictions = response.json()["predictions"] + else: + error = response.json()["error"] + raise DeepstackException(f"Error from Deepstack: {error}") diff --git a/setup.py b/setup.py index 2b7ae87..3861349 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -VERSION = "0.3" +VERSION = "0.4" REQUIRES = ["requests"] diff --git a/tests/test_deepstack.py b/tests/test_deepstack.py index 9d32804..cc3db02 100644 --- a/tests/test_deepstack.py +++ b/tests/test_deepstack.py @@ -1,5 +1,3 @@ -# Placeholder - import deepstack.core as ds import requests import requests_mock @@ -13,7 +11,7 @@ MOCK_API_KEY = "mock_api_key" MOCK_TIMEOUT = 8 -MOCK_RESPONSE = { +MOCK_OBJECT_DETECTION_RESPONSE = { "success": True, "predictions": [ { @@ -43,53 +41,87 @@ ], } -MOCK_PREDICTIONS = MOCK_RESPONSE["predictions"] -MOCK_CONFIDENCES = [0.6998661, 0.7996547] +MOCK_FACE_RECOGNITION_RESPONSE = { + "success": True, + "predictions": [ + { + "confidence": 0.74999994, + "userid": "Idris Elba", + "y_min": 176, + "x_min": 209, + "y_max": 825, + "x_max": 677, + }, + { + "confidence": 0, + "userid": "unknown", + "y_min": 230, + "x_min": 867, + "y_max": 729, + "x_max": 1199, + }, + ], +} + +MOCK_OBJECT_PREDICTIONS = MOCK_OBJECT_DETECTION_RESPONSE["predictions"] +MOCK_OBJECT_CONFIDENCES = [0.6998661, 0.7996547] CONFIDENCE_THRESHOLD = 0.7 +MOCK_RECOGNISED_FACES = {"Idris Elba": 75.0} -def test_DeepstackObject_process_image_bytes(): +def test_DeepstackObject_detect(): """Test a good response from server.""" with requests_mock.Mocker() as mock_req: - mock_req.post(MOCK_URL, status_code=ds.HTTP_OK, json=MOCK_RESPONSE) + mock_req.post( + MOCK_URL, status_code=ds.HTTP_OK, json=MOCK_OBJECT_DETECTION_RESPONSE + ) dsobject = ds.DeepstackObject(MOCK_IP_ADDRESS, MOCK_PORT) - dsobject.process_image_bytes(MOCK_BYTES) - assert dsobject.predictions == MOCK_PREDICTIONS + dsobject.detect(MOCK_BYTES) + assert dsobject.predictions == MOCK_OBJECT_PREDICTIONS -def test_DeepstackObject_process_image_bytes_timeout(): +def test_DeepstackObject_detect_timeout(): """Test a timeout. THIS SHOULD FAIL""" with pytest.raises(ds.DeepstackException) as excinfo: with requests_mock.Mocker() as mock_req: mock_req.post(MOCK_URL, exc=requests.exceptions.ConnectTimeout) dsobject = ds.DeepstackObject(MOCK_IP_ADDRESS, MOCK_PORT) - dsobject.process_image_bytes(MOCK_BYTES) + dsobject.detect(MOCK_BYTES) assert False assert "SHOULD FAIL" in str(excinfo.value) -def test_get_object_labels(): +def test_get_objects(): """Cant always be sure order of returned list items.""" - object_labels = ds.get_object_labels(MOCK_PREDICTIONS) - assert type(object_labels) is list - assert "dog" in object_labels - assert "person" in object_labels - assert len(object_labels) == 2 + objects = ds.get_objects(MOCK_OBJECT_PREDICTIONS) + assert type(objects) is list + assert "dog" in objects + assert "person" in objects + assert len(objects) == 2 def test_get_objects_summary(): - objects_summary = ds.get_objects_summary(MOCK_PREDICTIONS) + objects_summary = ds.get_objects_summary(MOCK_OBJECT_PREDICTIONS) assert objects_summary == {"dog": 1, "person": 2} -def test_get_label_confidences(): - label_confidences = ds.get_label_confidences(MOCK_PREDICTIONS, "person") - assert label_confidences == MOCK_CONFIDENCES +def test_get_object_confidences(): + object_confidences = ds.get_object_confidences(MOCK_OBJECT_PREDICTIONS, "person") + assert object_confidences == MOCK_OBJECT_CONFIDENCES def test_get_confidences_above_threshold(): assert ( - len(ds.get_confidences_above_threshold(MOCK_CONFIDENCES, CONFIDENCE_THRESHOLD)) + len( + ds.get_confidences_above_threshold( + MOCK_OBJECT_CONFIDENCES, CONFIDENCE_THRESHOLD + ) + ) == 1 ) + + +def test_get_recognised_faces(): + predictions = MOCK_FACE_RECOGNITION_RESPONSE["predictions"] + assert ds.get_recognised_faces(predictions) == MOCK_RECOGNISED_FACES diff --git a/usage-face-recognition.ipynb b/usage-face-recognition.ipynb index 3c3ce60..ca71c3f 100644 --- a/usage-face-recognition.ipynb +++ b/usage-face-recognition.ipynb @@ -4,20 +4,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This package provides convenience classes and functions for working with deepstack face API. \n", - "\n", - "Run deepstack with:\n", - "```\n", - "docker run -e VISION-FACE=True -e MODE=High -d \\\n", - " -v localstorage:/datastore -p 5000:5000 \\\n", - " -e API-KEY=\"Mysecretkey\" \\\n", - " --name deepstack deepquestai/deepstack:noavx\n", - "```" + "Work with the deepstack face API. " ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -30,14 +22,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "IP_ADDRESS = 'localhost'\n", "PORT = '5000'\n", "API_KEY = \"Mysecretkey\"\n", - "TIMEOUT = 8" + "TIMEOUT = 30 # Default is 10" ] }, { @@ -49,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -58,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -84,27 +76,29 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Perform face detection" + "## Face detection\n", + "Detect faces, but do not recognise them, quite fast." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[{'confidence': 0.9999846, 'y_min': 162, 'x_min': 1620, 'y_max': 680, 'x_max': 1982}, {'confidence': 0.99997175, 'y_min': 230, 'x_min': 867, 'y_max': 729, 'x_max': 1199}]\n" + "CPU times: user 5.04 ms, sys: 7.95 ms, total: 13 ms\n", + "Wall time: 3.01 s\n" ] } ], "source": [ - "#%%time\n", + "%%time\n", "try:\n", - " dsface.process_file(image_path)\n", - " print(dsface.predictions)\n", + " with open(image_path, \"rb\") as image_bytes:\n", + " dsface.detect(image_bytes)\n", "except ds.DeepstackException as exc:\n", " print(exc)" ] @@ -118,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -136,7 +130,7 @@ " 'x_max': 1199}]" ] }, - "execution_count": 8, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -147,7 +141,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -156,7 +150,7 @@ "2" ] }, - "execution_count": 11, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -165,12 +159,96 @@ "len(dsface.predictions)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Register a face\n", + "Post a name and a close up photo of a face" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 4.81 ms, sys: 9.05 ms, total: 13.9 ms\n", + "Wall time: 2.99 s\n" + ] + } + ], + "source": [ + "%%time\n", + "try:\n", + " with open('tests/images/idris.jpg', \"rb\") as image_bytes:\n", + " dsface.register_face(\"idris\", image_bytes)\n", + "except ds.DeepstackException as exc:\n", + " print(exc)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Face recognition\n", + "Recoginition will match any faces that have been taught. This is slower than face detection" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 4.37 ms, sys: 2.84 ms, total: 7.21 ms\n", + "Wall time: 3.87 s\n" + ] + } + ], + "source": [ + "%%time\n", + "try:\n", + " with open(image_path, \"rb\") as image_bytes:\n", + " dsface.recognise(image_bytes)\n", + "except ds.DeepstackException as exc:\n", + " print(exc)" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Helper functions\n", - "The package provides helper functions for extracting info out of deepstack predictions" + "The package provides helper functions for extracting info out of deepstack predictions.\n", + "\n", + "Get recognised faces and their probability (%)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Idris Elba': 74.7}" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds.get_recognised_faces(dsface.predictions)" ] }, { diff --git a/usage-object-detection.ipynb b/usage-object-detection.ipynb index 28b932e..e9bd621 100644 --- a/usage-object-detection.ipynb +++ b/usage-object-detection.ipynb @@ -4,22 +4,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This package provides convenience classes and functions for working with deepstack object detection API.\n", - "\n", - "Run deepstack with:\n", - "```\n", - "docker run -e VISION-DETECTION=True -e MODE=High -d \\\n", - " -v localstorage:/datastore -p 5000:5000 \\\n", - " -e API-KEY=\"Mysecretkey\" \\\n", - " --name deepstack deepquestai/deepstack:noavx\n", - "```\n", - "\n", - "Note that by default, the minimum confidence for detected objects is 0.45" + "Work with the deepstack object detection API (Yolo-v3). Note that by default, the minimum confidence for detected objects is 0.45" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -32,14 +22,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "IP_ADDRESS = 'localhost'\n", "PORT = '5000'\n", "API_KEY = \"Mysecretkey\"\n", - "TIMEOUT = 8" + "TIMEOUT = 20 # Default is 10" ] }, { @@ -51,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -60,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -86,27 +76,28 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Perform object detection" + "Perform object detection - can be slow" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[{'confidence': 0.9998661, 'label': 'person', 'y_min': 0, 'x_min': 258, 'y_max': 676, 'x_max': 485}, {'confidence': 0.9996547, 'label': 'person', 'y_min': 0, 'x_min': 405, 'y_max': 652, 'x_max': 639}, {'confidence': 0.99745613, 'label': 'dog', 'y_min': 311, 'x_min': 624, 'y_max': 591, 'x_max': 825}]\n" + "CPU times: user 5.14 ms, sys: 3.16 ms, total: 8.3 ms\n", + "Wall time: 13.6 s\n" ] } ], "source": [ - "#%%time\n", + "%%time\n", "try:\n", - " dsobject.process_file(image_path)\n", - " print(dsobject.predictions)\n", + " with open(image_path, 'rb') as image_bytes:\n", + " dsobject.detect(image_bytes)\n", "except ds.DeepstackException as exc:\n", " print(exc)" ] @@ -120,7 +111,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -146,7 +137,7 @@ " 'x_max': 825}]" ] }, - "execution_count": 7, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -157,7 +148,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -166,7 +157,7 @@ "3" ] }, - "execution_count": 14, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -187,12 +178,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Get the set of unique labels" + "Get the set objects" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -201,25 +192,25 @@ "['dog', 'person']" ] }, - "execution_count": 9, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "ds.get_object_labels(dsobject.predictions)" + "ds.get_objects(dsobject.predictions)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Get a summary of the number of occurances of labels" + "Get a summary of the number of occurances of objects" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -228,7 +219,7 @@ "{'dog': 1, 'person': 2}" ] }, - "execution_count": 10, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -238,36 +229,16 @@ "summary" ] }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['dog', 'person']" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "list(summary.keys())" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Get a list of confidences for a single label type, e.g. `person`" + "Get a list of confidences for a single object type, e.g. `person`" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -276,14 +247,14 @@ "[0.9998661, 0.9996547]" ] }, - "execution_count": 12, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "confidences = ds.get_label_confidences(dsobject.predictions, 'person')\n", - "confidences" + "person_confidences = ds.get_object_confidences(dsobject.predictions, 'person')\n", + "person_confidences" ] }, { @@ -295,7 +266,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 22, "metadata": {}, "outputs": [ { @@ -304,14 +275,14 @@ "1" ] }, - "execution_count": 16, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "CONFIDENCE_THRESHOLD = 0.9997\n", - "len(ds.get_confidences_above_threshold(confidences, CONFIDENCE_THRESHOLD))" + "len(ds.get_confidences_above_threshold(person_confidences, CONFIDENCE_THRESHOLD))" ] }, {