
{"id":2966,"date":"2017-02-28T06:00:57","date_gmt":"2017-02-28T14:00:57","guid":{"rendered":"http:\/\/briantroy.com\/?p=2966"},"modified":"2025-01-03T04:28:06","modified_gmt":"2025-01-03T04:28:06","slug":"person-recognition-in-images-analysis","status":"publish","type":"post","link":"https:\/\/blogarchive.briantroy.com\/index.php\/2017\/02\/28\/person-recognition-in-images-analysis\/","title":{"rendered":"Person Recognition in Images with OpenCV &amp; Neo4j"},"content":{"rendered":"<p>Time for an update on my ongoing person identification in images project; for all the background you can check out these previous posts:<\/p>\n<p><a href=\"http:\/\/briantroy.com\/2017\/01\/05\/analyzing-aws-rekognition-accuracy-with-neo4j\/\">Analyzing AWS Rekognition Accuracy with Neo4j<\/a><\/p>\n<p><a href=\"http:\/\/briantroy.com\/2017\/01\/08\/aws-rekognition-graph-analysis-person-label-accuracy\/\">AWS Rekognition Graph Analysis \u2013 Person Label Accuracy<\/a><\/p>\n<p><a href=\"http:\/\/briantroy.com\/2017\/01\/19\/person-recognition-opencv-vs-aws-rekognition\/\">Person Recognition: OpenCV vs. AWS Rekognition<\/a><\/p>\n<p>In my <a href=\"https:\/\/briantroy.com\/2016\/10\/25\/serverless-architecture-practical-iot-implementation\/\">earlier serverless series<\/a> I discussed and provided code for getting images into S3 and processed by AWS Rekognition &#8211; including storing the Rekognition label data in DynamoDB.<\/p>\n<p>This post builds on all of those concepts.<\/p>\n<p>In short &#8211; I&#8217;ve been collecting comparative data on person recognition using <a href=\"https:\/\/aws.amazon.com\/rekognition\/\">AWS Rekognition<\/a> and <a href=\"http:\/\/opencv.org\">OpenCV<\/a> and storing\u00a0that data in <a href=\"http:\/\/neo4j.com\">Neo4j<\/a> for analysis.<\/p>\n<p><!--more--><\/p>\n<p>My assumption was that the trained model for person detection that ships with OpenCV was efficient enough to run &#8220;embedded&#8221;. In order to validate that I decided to run OpenCV and the person detection algorithm on a Raspberry PI located on-site. If you&#8217;ve read my earlier series of posts you know a Raspberry PI also served as a local controller node in my serverless IoT environment.<\/p>\n<p>I used the following installation guide for OpenCV on a Raspberry PI to get started:<\/p>\n<p><a href=\"http:\/\/www.pyimagesearch.com\/2015\/07\/27\/installing-opencv-3-0-for-both-python-2-7-and-python-3-on-your-raspberry-pi-2\/\">http:\/\/www.pyimagesearch.com\/2015\/07\/27\/installing-opencv-3-0-for-both-python-2-7-and-python-3-on-your-raspberry-pi-2\/<\/a><\/p>\n<p>One additional note: the build takes up a lot of space &#8211; I strongly recommend you do the build on an external USB drive. It won&#8217;t be fast, but it won&#8217;t fill your root directory&#8230;<\/p>\n<h2>Queuing Images for OpenCV Processing<\/h2>\n<p>Two things were important when considering how to let the OpenCV processor know there were\u00a0images to process:<\/p>\n<ol>\n<li>Not interfere with the work already being done in the serverless IoT system<\/li>\n<li>Not transmit the image across the internet multiple times<\/li>\n<\/ol>\n<p>I solved this with two simple steps:<\/p>\n<h3>NAS Storage and Mapping<\/h3>\n<p>The first step was to ensure the second Raspberry PI had the exact same mapping to my NAS that the local controller node did. This allowed both PIs to reference the image file with the exact same path.<\/p>\n<h3>SQS Messaging for Files<\/h3>\n<p>Since the local controller node was already detecting the arrival of the file I saw no benefit in having the second PI detect the arrival event. In order to accomplish this I leveraged another <a href=\"https:\/\/aws.amazon.com\/sqs\/\">AWS service &#8211; SQS<\/a>.<\/p>\n<p>With a minimum of code I could add the message to SQS from the IoT local controller node and receive the message on the OpenCV PI.<\/p>\n<h4>Code to Create the SQS Message<\/h4>\n<p>I modified the <a href=\"https:\/\/github.com\/briantroy\/sendftpfilestos3\/blob\/graph-analytics\/ftpfiletos3.py\">daemon on the IoT Controller<\/a> by adding the following function:<\/p>\n<pre>def put_file_info_on_sqs(object_info, logger, app_config):\n    # Get the service resource\n    import boto3\n    import json\n\n    if object_info['img_type'] == 'snap':\n        sqs = boto3.resource('sqs')\n\n        # Get the queue\n        queue = sqs.get_queue_by_name(QueueName='image_for_person_detection')\n        logger.info(\"Putting message: {} on queue.\".format(json.dumps(object_info)))\n        response = queue.send_message(MessageBody=json.dumps(object_info))\n    # Fin\n<\/pre>\n<p>At the appropriate time in the processing sequence I simply call this function and an SQS message is created on the named queue.<\/p>\n<h2>Processing Images with OpenCV<\/h2>\n<p>In order to process the images with OpenCV I created a daemon that would read the SQS queue and process each image. In addition the information obtained would be added to the Neo4j Graph.<\/p>\n<h3>Reading Message from SQS<\/h3>\n<pre>def get_sqs_messages():\n    \"\"\"\n    Gets messages from AWS SQS and sends them along for processing.\n    :return:\n    \"\"\"\n    # Get the service resource\n    sqs = boto3.resource('sqs')\n\n    # Get the queue\n    queue = sqs.get_queue_by_name(QueueName='image_for_person_detection')\n\n    for message in queue.receive_messages(MessageAttributeNames=['Author']):\n        image_info = json.loads(message.body)\n        if image_info['img_type'] == \"snap\":\n            logger.info(\"Processing message (message id: {})for file: {}\".format(message.message_id,\n                                                                                 image_info['file_name']))\n            image_info['message_id'] = message.message_id\n            threading.Thread(name=('message_processor-' + message.message_id), target=process_image_message,\n                             args=(image_info,)).start()\n            # process_image_message(image_info)\n        # FIN\n\n        message.delete()\n    # End For\n# end get_sqs_messages\n<\/pre>\n<p>This code reads the message from SQS and starts a processing thread for the image.<\/p>\n<h3>Processing with OpenCV<\/h3>\n<p>The following code processes the image with OpenCV:<\/p>\n<pre>def detect_people(image_path):\n    \"\"\"\n    Execution of the OpenCV person detection algorithm.\n    :param image_path:\n    :return:\n    \"\"\"\n    # HOG detector config values\n    image_size = get_config_item(app_config, \"opencv_processing_info.image_size\")\n    win_stride = get_config_item(app_config, \"opencv_processing_info.win_stride\")\n    padding = get_config_item(app_config, \"opencv_processing_info.padding\")\n    scale = get_config_item(app_config, \"opencv_processing_info.scale\")\n\n    # initialize the HOG descriptor\/person detector\n    hog = cv2.HOGDescriptor()\n    hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())\n\n    return_info = None\n\n    image_start = time.time()\n    # load the image and resize it to (1) reduce detection time\n    # and (2) improve detection accuracy\n    filename = image_path[image_path.rfind(\"\/\") + 1:]\n    # print(\"[INFO] Processing file: {}\".format(imagePath))\n    image = cv2.imread(image_path)\n\n    if image is not None:\n        return_info = {}\n        full_image = image.copy()\n        scale_ratio = 1\n        if image_size != 'original':\n            image = imutils.resize(image, width=min(image_size, image.shape[1]))\n            scale_ratio = float(full_image.shape[1]) \/ float(image_size)\n        orig = image.copy()\n\n        # detect people in the image\n        people = hog.detectMultiScale(image, winStride=(win_stride, win_stride),\n                                      padding=(padding, padding), scale=scale)\n\n        logger.info(\"People detection done for image {} in {} seconds\".format(\n            image_path, (time.time() - image_start)\n        ))\n\n        rects = people[0]\n        weights = people[1]\n        return_info['rectangles'] = rects\n        return_info['weights'] = weights\n        return_info['image_dimensions'] = image.shape\n\n        # draw the original bounding boxes\n        for (x, y, w, h) in rects:\n            cv2.rectangle(orig, (x, y), (x + w, y + h), (0, 0, 255), 2)\n\n        # apply non-maxima suppression to the bounding boxes using a\n        # fairly large overlap threshold to try to maintain overlapping\n        # boxes that are still people\n        rects = np.array([[x, y, x + w, y + h] for (x, y, w, h) in rects])\n        pick = non_max_suppression(rects, probs=None, overlapThresh=0.65)\n        return_info['number_merged_rectangles'] = len(pick)\n\n        # draw the final bounding boxes\n        for (xA, yA, xB, yB) in pick:\n            xA *= scale_ratio\n            yA *= scale_ratio\n            xB *= scale_ratio\n            yB *= scale_ratio\n            cv2.rectangle(full_image, (int(xA), int(yA)), (int(xB), int(yB)), (0, 255, 0), 2)\n        # End for\n\n        if len(pick) &gt; 0:\n            # show some information on the number of bounding boxes\n            logger.info(\"[MATCH] {}: found {} person with {} boxes returned from HOG - confidence {}\".format(\n                filename, len(pick), len(rects), weights))\n            cv2.imwrite(\"\/tmp\/full_\" + filename, full_image)\n            return_info['annotated_image_file'] = \"\/tmp\/full_\" + filename\n        # FIN\n\n        return_info['processing_time'] = time.time() - image_start\n        logger.info(\"Image processing for {} done in {} seconds\".format(\n            image_path, (time.time() - image_start)\n        ))\n    # Fin\n    return return_info\n<\/pre>\n<p>This is quite a bit to digest &#8211; and I recommend you read up on OpenCV as you review this, but I&#8217;ll break it down a bit.<\/p>\n<p>The first section sets\u00a0the configurable values for the detector. These are loaded earlier from a configuration file. The OpenCV documentation provides excellent documentation of the configuration parameters.<\/p>\n<p>We then scale the image to reduce detection time. We retain the ratio so we can later draw the detection box on the full size\/resolution image.<\/p>\n<p>The actual detection happens with a single line of code:<\/p>\n<pre>        # detect people in the image\n        people = hog.detectMultiScale(image, winStride=(win_stride, win_stride),\n                                      padding=(padding, padding), scale=scale)\n<\/pre>\n<p>After that we do some non-maxima suppression to merge boxes within which people are detected.<\/p>\n<p>Lastly, we draw the detected box on the original scale image (by de-scaling the box), log some information and return the resulting data.<\/p>\n<h2>Storing the Results in Neo4j<\/h2>\n<p>Once the detection is complete we store the data in Neo4j for later analysis. In short, we want to generate Cypher to update the graph that looks like this:<\/p>\n<pre>MERGE(this_image:Image {object_key: \"patrolcams\/DownDownC1\/2017-02-27\/Hour-17\/snap\/SDAlarm_20170227-174726.jpg\", timestamp: 1488246446})  MERGE(this_label:Label {label_name: \"OpenCV Person\"})  MERGE (this_image)-[:HAS_NOT_OPENCV_LABEL {confidence: [], number_found: \"0\", opencv_win_stride: 4, opencv_image_size: 400, opencv_padding: 32, opencv_scale: 1.05, opencv_processing_time: 2.00539588928 }]-&gt;(this_label)\n<\/pre>\n<p>This creates (or uses) the image node for the image, the label node which is the indication that OpenCV processed the image, and connects them via one of two edges:<\/p>\n<ul>\n<li>HAS_NOT_OPENCV_LABEL &#8211; no person detected<\/li>\n<li>HAS_OPEN_CV_LABEL &#8211; a person was detected<\/li>\n<\/ul>\n<p>On that edge we put all of the current configurable values for the detector (so we can evaluate performance across configuration changes) and the processing time for the detector.<\/p>\n<p>The code used to generate the cypher is:<\/p>\n<pre>def add_info_to_graph(image_info, person_found = True):\n    \"\"\"\n    Adds the OpenCV output information to the local graph store for analysis\n    :param image_info: The dict containing the information from the SQS message\n    ;param person_found: Default True. Indicates if a person was found in the image.\n    :return:\n    \"\"\"\n\n    start_timing = time.time()\n    logger.info(\"Updating the analytics graph for {}\".format(image_info['s3_object']))\n    s3_object = \"patrolcams\" + \\\n                '\/' + image_info['camera_name'] + '\/' + \\\n                image_info['date_string'] + '\/' + \\\n                image_info['hour_string'] + '\/' + \\\n                image_info['img_type'] + '\/' + \\\n                image_info['just_file']\n\n    add_image_node = 'MERGE(this_image:Image {object_key: \"' + s3_object + '\", ' + \\\n                     'timestamp: ' + str(image_info['utc_ts']) + '}) '\n    add_label_node = 'MERGE(this_label:Label {label_name: \"OpenCV Person\"}) '\n\n    increment = 0\n    string_confidence_array = \"\"\n    for confidence in image_info['opencv_person_detection_info']['weights']:\n        if increment &gt; 0:\n            string_confidence_array += \",\"\n\n        string_confidence_array += str(confidence[0])\n        increment += 1\n    # End For\n\n    label_name = ':HAS_OPENCV_LABEL'\n    if not person_found:\n        label_name = ':HAS_NOT_OPENCV_LABEL'\n    # Fin\n\n    relate_image_to_label = 'MERGE (this_image)-[' + label_name + ' {confidence: [' + \\\n                            string_confidence_array + '], ' + \\\n                            'number_found: \"' + \\\n                            str(image_info['opencv_person_detection_info']['number_merged_rectangles']) + \\\n                            '\", opencv_win_stride: ' + str(get_config_item(app_config,\n                                                                       \"opencv_processing_info.win_stride\")) + ', ' + \\\n                            'opencv_image_size: ' + str(\n                                                        image_info['opencv_person_detection_info']['image_dimensions'][1]\n                                                    ) + ', ' + \\\n                            'opencv_padding: ' + str(get_config_item(app_config,\n                                                                 \"opencv_processing_info.padding\")) + ', ' + \\\n                            'opencv_scale: ' + str(get_config_item(app_config,\n                                                                 \"opencv_processing_info.scale\")) + ', ' + \\\n                            'opencv_processing_time: ' + \\\n                            str(image_info['opencv_person_detection_info']['processing_time']) + \\\n                            ' }]-&gt;(this_label)'\n\n    full_query_list = add_image_node + \" \" + add_label_node + \" \" + relate_image_to_label\n\n    logger.info(\"Full Cypher Query: {}\".format(full_query_list))\n    neo_session = driver.session()\n\n    tx = neo_session.begin_transaction()\n\n    tx.run(full_query_list)\n\n    tx.commit()\n    neo_session.close()\n\n    logger.info(\"Graph updated for {} in {} seconds.\".format(image_info['s3_object'],\n                                                             time.time() - start_timing))\n    return True\n<\/pre>\n<h2>Results<\/h2>\n<p>Over the last 30 days this setup has processed 103,137 images with OpenCV. 36,313 contained a person, 66,824 did not (at least based on the output of the detector).<\/p>\n<p>Using the following cypher I&#8217;m able to see how many were processed and the average processing time per image:<\/p>\n<pre>match(camera:Camera)-[:HAS_IMAGE]-(image:Image)-[image_label_edge]-(label:Label) where label.label_name in['Person', 'OpenCV Person'] and image.timestamp &gt; ((timestamp()\/1000) - 60*60*24*30) return label.label_name, type(image_label_edge), count(type(image_label_edge)), avg(image_label_edge.opencv_processing_time)\n<\/pre>\n<p>This results in the following output:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-3042 size-full\" src=\"https:\/\/blogarchive.briantroy.com\/wp-content\/uploads\/2017\/02\/screenshot-2017-02-27-17-57-08-2.png\" alt=\"screenshot-2017-02-27-17-57-08\" width=\"3268\" height=\"688\" \/><\/p>\n<p>The images process in an average of ~3 seconds &#8211; which is quite good given the limited processing capacity of the Raspberry PI and the configuration set I&#8217;m using.<\/p>\n<p>The line with the label_name &#8220;Person&#8221; indicates the number of images AWS Rekognition detected a person in over the same time period.<\/p>\n<p>You&#8217;ll note the (very interesting) ~12,000 difference in person detection &#8211; more on that in a later post.<\/p>\n<h2>The Payoff<\/h2>\n<p>Since no N3o4j post is complete without some graph image eye-candy &#8211; here is a heat map of where OpenCV has detected people in the last hour:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-3052 size-full\" src=\"https:\/\/blogarchive.briantroy.com\/wp-content\/uploads\/2017\/02\/screenshot-2017-02-27-18-03-38-2.png\" alt=\"Screenshot 2017-02-27 18.03.38.png\" width=\"2946\" height=\"1610\" \/><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Time for an update on my ongoing person identification in images project; for all the background you can check out these previous posts: Analyzing AWS Rekognition Accuracy with Neo4j AWS Rekognition Graph Analysis \u2013 Person Label Accuracy Person Recognition: OpenCV vs. AWS Rekognition In my earlier serverless series I discussed and provided code for getting&hellip; <a class=\"more-link\" href=\"https:\/\/blogarchive.briantroy.com\/index.php\/2017\/02\/28\/person-recognition-in-images-analysis\/\">Continue reading <span class=\"screen-reader-text\">Person Recognition in Images with OpenCV &amp; Neo4j<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":3052,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3,6,9,19,22,28],"tags":[54,81,146,198,233],"_links":{"self":[{"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/posts\/2966"}],"collection":[{"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/comments?post=2966"}],"version-history":[{"count":1,"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/posts\/2966\/revisions"}],"predecessor-version":[{"id":3302,"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/posts\/2966\/revisions\/3302"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/media\/3052"}],"wp:attachment":[{"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/media?parent=2966"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/categories?post=2966"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogarchive.briantroy.com\/index.php\/wp-json\/wp\/v2\/tags?post=2966"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}