From e5e5771091d0384bbee39a07bb21313cb7a33346 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Wed, 30 Jun 2021 18:33:39 -0700 Subject: [PATCH] More on stock detection --- .../lessons/2-store-location-data/README.md | 37 +++- .../lessons/1-train-fruit-detector/README.md | 2 + .../lessons/2-check-stock-device/README.md | 14 +- .../pi/fruit-quality-detector/app.py | 92 ++++++++++ .../fruit-quality-detector/app.py | 92 ++++++++++ .../fruit-quality-detector/.gitignore | 5 + .../.vscode/extensions.json | 7 + .../fruit-quality-detector/include/README | 39 +++++ .../fruit-quality-detector/lib/README | 46 +++++ .../fruit-quality-detector/platformio.ini | 26 +++ .../fruit-quality-detector/src/camera.h | 160 +++++++++++++++++ .../fruit-quality-detector/src/config.h | 49 ++++++ .../fruit-quality-detector/src/main.cpp | 129 ++++++++++++++ .../fruit-quality-detector/test/README | 11 ++ .../pi/fruit-quality-detector/app.py | 2 +- .../fruit-quality-detector/app.py | 2 +- .../single-board-computer-count-stock.md | 161 ++++++++++++++++++ .../single-board-computer-object-detector.md | 2 +- .../wio-terminal-count-stock.md | 3 + images/rpi-stock-with-bounding-boxes.jpg | Bin 0 -> 31556 bytes 20 files changed, 873 insertions(+), 6 deletions(-) create mode 100644 5-retail/lessons/2-check-stock-device/code-count/pi/fruit-quality-detector/app.py create mode 100644 5-retail/lessons/2-check-stock-device/code-count/virtual-iot-device/fruit-quality-detector/app.py create mode 100644 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/.gitignore create mode 100644 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/.vscode/extensions.json create mode 100644 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/include/README create mode 100644 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/lib/README create mode 100644 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/platformio.ini create mode 100644 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/camera.h create mode 100644 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/config.h create mode 100644 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/main.cpp create mode 100644 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/test/README create mode 100644 5-retail/lessons/2-check-stock-device/single-board-computer-count-stock.md create mode 100644 5-retail/lessons/2-check-stock-device/wio-terminal-count-stock.md create mode 100644 images/rpi-stock-with-bounding-boxes.jpg diff --git a/3-transport/lessons/2-store-location-data/README.md b/3-transport/lessons/2-store-location-data/README.md index 44f77691..ba4ca52b 100644 --- a/3-transport/lessons/2-store-location-data/README.md +++ b/3-transport/lessons/2-store-location-data/README.md @@ -18,6 +18,7 @@ In this lesson we'll cover: * [Structured and unstructured data](#structured-and-unstructured-data) * [Send GPS data to an IoT Hub](#send-gps-data-to-an-iot-hub) +* [Hot, warm, and cold paths](#hot-warm-and-cold-paths) * [Handle GPS events using serverless code](#handle-gps-events-using-serverless-code) * [Azure Storage Accounts](#azure-storage-accounts) * [Connect your serverless code to storage](#connect-your-serverless-code-to-storage) @@ -44,6 +45,8 @@ Imagine you were adding IoT devices to a fleet of vehicles for a large commercia This data can change constantly. For example, if the IoT device is in a truck cab, then the data it sends may change as the trailer changes, for example only sending temperature data when a refrigerated trailer is used. +✅ What other IoT data might be captured? Think about the kinds of loads trucks can carry, as well as maintenance data. + This data varies from vehicle to vehicle, but it all gets sent to the same IoT service for processing. The IoT service needs to be able to process this unstructured data, storing it in a way that allows it to be searched or analyzed, but works with different structures to this data. ### SQL vs NoSQL storage @@ -58,10 +61,14 @@ The first databases were Relational Database Management Systems (RDBMS), or rela For example, if you stored a users personal details in a table, you would have some kind of internal unique ID per user that is used in a row in a table that contains the users name and address. If you then wanted to store other details about that user, such as their purchases, in another table, you would have one column in the new table for that users ID. When you look up a user, you can use their ID to get their personal details from one table, and their purchases from another. -SQL databases are ideal for storing structured data, and for when you want to ensure the data matches your schema. Some well known SQL databases are Microsoft SQL Server, MySQL, and PostgreSQL. +SQL databases are ideal for storing structured data, and for when you want to ensure the data matches your schema. ✅ If you haven't used SQL before, take a moment to read up on it on the [SQL page on Wikipedia](https://wikipedia.org/wiki/SQL). +Some well known SQL databases are Microsoft SQL Server, MySQL, and PostgreSQL. + +✅ Do some research: Read up on some of these SQL databases and their capabilities. + #### NoSQL database NoSQL databases are called NoSQL because they don't have the same rigid structure of SQL databases. They are also known as document databases as they can store unstructured data such as documents. @@ -74,6 +81,8 @@ NoSQL database do not have a pre-defined schema that limits how data is stored, Some well known NoSQL databases include Azure CosmosDB, MongoDB, and CouchDB. +✅ Do some research: Read up on some of these NoSQL databases and their capabilities. + In this lesson, you will be using NoSQL storage to store IoT data. ## Send GPS data to an IoT Hub @@ -136,9 +145,33 @@ message = Message(json.dumps(message_json)) Run your device code and ensure messages are flowing into IoT Hub using the `az iot hub monitor-events` CLI command. +## Hot, warm, and cold paths + +Data that flows from an IoT device to the cloud is not always processed in real time. Some data needs real time processing, other data can be processed a short time later, and other data can be processed much later. The flow of data to different services that process the data at different times is referred to hot, warm and cold paths. + +### Hot path + +The hot path refers to data that needs to be processed in real time or near real time. You would use hot path data for alerts, such as getting alerts that a vehicle is approaching a depot, or that the temperature in a refrigerated truck is too high. + +To use hot path data, your code would respond to events as soon as they are received by your cloud services. + +### Warm path + +The warm path refers to data that can be processed a short while after being received, for example for reporting or short term analytics. You would use warm path data for daily reports on vehicle mileage, using data gathered the previous day. + +Warm path data is stored once it is received by the cloud service inside some kind of storage that can be quickly accessed. + +### Cold path + +THe cold path refers to historic data, storing data for the long term to be processed whenever needed. For example, you could use the cold path to get annual mileage reports for vehicles, or run analytics on routes to find the most optimal route to reduce fuel costs. + +Cold path data is stored in data warehouses - databases designed for storing large amounts of data that will never change and can be queried quickly and easily. You would normally have a regular job in your cloud application that would run at a regular time each day, week, or month to move data from warm path storage into the data warehouse. + +✅ Think about the data you have captured so far in these lessons. Is it hot, warm or cold path data? + ## Handle GPS events using serverless code -Once data is flowing into your IoT Hub, you can write some serverless code to listen for events published to the Event-Hub compatible endpoint. +Once data is flowing into your IoT Hub, you can write some serverless code to listen for events published to the Event-Hub compatible endpoint. This is the warm path - this data will be stored and used in the next lesson for reporting on the journey. ![Sending GPS telemetry from an IoT device to IoT Hub, then to Azure Functions via an event hub trigger](../../../images/gps-telemetry-iot-hub-functions.png) diff --git a/4-manufacturing/lessons/1-train-fruit-detector/README.md b/4-manufacturing/lessons/1-train-fruit-detector/README.md index 8ab846cc..90a12c7d 100644 --- a/4-manufacturing/lessons/1-train-fruit-detector/README.md +++ b/4-manufacturing/lessons/1-train-fruit-detector/README.md @@ -74,6 +74,8 @@ ML models don't give a binary answer, instead they give probabilities. For examp The ML model used to detect images like this is called an *image classifier* - it is given labelled images, and then classifies new images based off these labels. +> 💁 This is an over-simplification, and there are many other ways to train models that don't always need labelled outputs, such as unsupervised learning. If you want to learn more about ML, check out [ML for beginners, a 24 lesson curriculum on Machine Learning](https://aka.ms/ML-beginners). + ## Train an image classifier To successfully train an image classifier you need millions of images. As it turns out, once you have an image classifier trained on millions or billions of assorted images, you can re-use it and re-train it using a small set of images and get great results, using a process called *transfer learning*. diff --git a/5-retail/lessons/2-check-stock-device/README.md b/5-retail/lessons/2-check-stock-device/README.md index 70423b49..a7883c0f 100644 --- a/5-retail/lessons/2-check-stock-device/README.md +++ b/5-retail/lessons/2-check-stock-device/README.md @@ -122,7 +122,7 @@ In the example above, one bounding box indicated a predicted can of tomato paste ## Retrain the model -Just like with the image classifier, you can retrain your model using data captured by your IoT device. Using this real-world data will ensure your model works well when used from your IoT device. +Like with the image classifier, you can retrain your model using data captured by your IoT device. Using this real-world data will ensure your model works well when used from your IoT device. Unlike with the image classifier, you can't just tag an image. Instead you need to review every bounding box detected by the model. If the box is around the wrong thing then it needs to be deleted, if it is in the wrong location it needs to be adjusted. @@ -146,16 +146,28 @@ Using a combination of the number of objects detected and the bounding boxes, yo ### Task - count stock +Follow the relevant guide below to count stock using the results from the object detector from your IoT device: + +* [Arduino - Wio Terminal](wio-terminal-count-stock.md) +* [Single-board computer - Raspberry Pi/Virtual device](single-board-computer-count-stock.md) + --- ## 🚀 Challenge +Can you detect incorrect stock? Train your model on multiple objects, then update your app to alert you if the wrong stock is detected. + +Maybe even take this further and detect stock side by side on the same shelf, and see if something has been put in the wrong place bu defining limits on the bounding boxes. + ## Post-lecture quiz [Post-lecture quiz](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/40) ## Review & Self Study +* Learn more about how to architect an end-to-end stock detection system from the [Out of stock detection at the edge pattern guide on Microsoft Docs](https://docs.microsoft.com/hybrid/app-solutions/pattern-out-of-stock-at-edge?WT.mc_id=academic-17441-jabenn) +* Learn other ways to build end-to-end retail solutions combining a range of IoT and cloud services by watching this [Behind the scenes of a retail solution - Hands On! video on YouTube](https://www.youtube.com/watch?v=m3Pc300x2Mw). + ## Assignment [Use your object detector on the edge](assignment.md) diff --git a/5-retail/lessons/2-check-stock-device/code-count/pi/fruit-quality-detector/app.py b/5-retail/lessons/2-check-stock-device/code-count/pi/fruit-quality-detector/app.py new file mode 100644 index 00000000..74ab558a --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/pi/fruit-quality-detector/app.py @@ -0,0 +1,92 @@ +import io +import time +from picamera import PiCamera + +from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient +from msrest.authentication import ApiKeyCredentials + +from PIL import Image, ImageDraw, ImageColor +from shapely.geometry import Polygon + +camera = PiCamera() +camera.resolution = (640, 480) +camera.rotation = 0 + +time.sleep(2) + +image = io.BytesIO() +camera.capture(image, 'jpeg') +image.seek(0) + +with open('image.jpg', 'wb') as image_file: + image_file.write(image.read()) + +prediction_url = '' +prediction_key = '' + +parts = prediction_url.split('/') +endpoint = 'https://' + parts[2] +project_id = parts[6] +iteration_name = parts[9] + +prediction_credentials = ApiKeyCredentials(in_headers={"Prediction-key": prediction_key}) +predictor = CustomVisionPredictionClient(endpoint, prediction_credentials) + +image.seek(0) +results = predictor.detect_image(project_id, iteration_name, image) + +threshold = 0.3 + +predictions = list(prediction for prediction in results.predictions if prediction.probability > threshold) + +for prediction in predictions: + print(f'{prediction.tag_name}:\t{prediction.probability * 100:.2f}%') + +overlap_threshold = 0.002 + +def create_polygon(prediction): + scale_left = prediction.bounding_box.left + scale_top = prediction.bounding_box.top + scale_right = prediction.bounding_box.left + prediction.bounding_box.width + scale_bottom = prediction.bounding_box.top + prediction.bounding_box.height + + return Polygon([(scale_left, scale_top), (scale_right, scale_top), (scale_right, scale_bottom), (scale_left, scale_bottom)]) + +to_delete = [] + +for i in range(0, len(predictions)): + polygon_1 = create_polygon(predictions[i]) + + for j in range(i+1, len(predictions)): + polygon_2 = create_polygon(predictions[j]) + overlap = polygon_1.intersection(polygon_2).area + + smallest_area = min(polygon_1.area, polygon_2.area) + + if overlap > (overlap_threshold * smallest_area): + to_delete.append(predictions[i]) + break + +for d in to_delete: + predictions.remove(d) + +print(f'Counted {len(predictions)} stock items') + + +with Image.open('image.jpg') as im: + draw = ImageDraw.Draw(im) + + for prediction in predictions: + scale_left = prediction.bounding_box.left + scale_top = prediction.bounding_box.top + scale_right = prediction.bounding_box.left + prediction.bounding_box.width + scale_bottom = prediction.bounding_box.top + prediction.bounding_box.height + + left = scale_left * im.width + top = scale_top * im.height + right = scale_right * im.width + bottom = scale_bottom * im.height + + draw.rectangle([left, top, right, bottom], outline=ImageColor.getrgb('red'), width=2) + + im.save('image.jpg') diff --git a/5-retail/lessons/2-check-stock-device/code-count/virtual-iot-device/fruit-quality-detector/app.py b/5-retail/lessons/2-check-stock-device/code-count/virtual-iot-device/fruit-quality-detector/app.py new file mode 100644 index 00000000..37b464ea --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/virtual-iot-device/fruit-quality-detector/app.py @@ -0,0 +1,92 @@ +from counterfit_connection import CounterFitConnection +CounterFitConnection.init('127.0.0.1', 5000) + +import io +from counterfit_shims_picamera import PiCamera + +from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient +from msrest.authentication import ApiKeyCredentials + +from PIL import Image, ImageDraw, ImageColor +from shapely.geometry import Polygon + +camera = PiCamera() +camera.resolution = (640, 480) +camera.rotation = 0 + +image = io.BytesIO() +camera.capture(image, 'jpeg') +image.seek(0) + +with open('image.jpg', 'wb') as image_file: + image_file.write(image.read()) + +prediction_url = '' +prediction_key = '' + +parts = prediction_url.split('/') +endpoint = 'https://' + parts[2] +project_id = parts[6] +iteration_name = parts[9] + +prediction_credentials = ApiKeyCredentials(in_headers={"Prediction-key": prediction_key}) +predictor = CustomVisionPredictionClient(endpoint, prediction_credentials) + +image.seek(0) +results = predictor.detect_image(project_id, iteration_name, image) + +threshold = 0.3 + +predictions = list(prediction for prediction in results.predictions if prediction.probability > threshold) + +for prediction in predictions: + print(f'{prediction.tag_name}:\t{prediction.probability * 100:.2f}%') + +overlap_threshold = 0.002 + +def create_polygon(prediction): + scale_left = prediction.bounding_box.left + scale_top = prediction.bounding_box.top + scale_right = prediction.bounding_box.left + prediction.bounding_box.width + scale_bottom = prediction.bounding_box.top + prediction.bounding_box.height + + return Polygon([(scale_left, scale_top), (scale_right, scale_top), (scale_right, scale_bottom), (scale_left, scale_bottom)]) + +to_delete = [] + +for i in range(0, len(predictions)): + polygon_1 = create_polygon(predictions[i]) + + for j in range(i+1, len(predictions)): + polygon_2 = create_polygon(predictions[j]) + overlap = polygon_1.intersection(polygon_2).area + + smallest_area = min(polygon_1.area, polygon_2.area) + + if overlap > (overlap_threshold * smallest_area): + to_delete.append(predictions[i]) + break + +for d in to_delete: + predictions.remove(d) + +print(f'Counted {len(predictions)} stock items') + + +with Image.open('image.jpg') as im: + draw = ImageDraw.Draw(im) + + for prediction in predictions: + scale_left = prediction.bounding_box.left + scale_top = prediction.bounding_box.top + scale_right = prediction.bounding_box.left + prediction.bounding_box.width + scale_bottom = prediction.bounding_box.top + prediction.bounding_box.height + + left = scale_left * im.width + top = scale_top * im.height + right = scale_right * im.width + bottom = scale_bottom * im.height + + draw.rectangle([left, top, right, bottom], outline=ImageColor.getrgb('red'), width=2) + + im.save('image.jpg') diff --git a/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/.gitignore b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/.gitignore new file mode 100644 index 00000000..89cc49cb --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/.vscode/extensions.json b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/.vscode/extensions.json new file mode 100644 index 00000000..0f0d7401 --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ] +} diff --git a/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/include/README b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/include/README new file mode 100644 index 00000000..194dcd43 --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/lib/README b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/lib/README new file mode 100644 index 00000000..6debab1e --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/platformio.ini b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/platformio.ini new file mode 100644 index 00000000..5f3eb8a7 --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/platformio.ini @@ -0,0 +1,26 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:seeed_wio_terminal] +platform = atmelsam +board = seeed_wio_terminal +framework = arduino +lib_deps = + seeed-studio/Seeed Arduino rpcWiFi @ 1.0.5 + seeed-studio/Seeed Arduino FS @ 2.0.3 + seeed-studio/Seeed Arduino SFUD @ 2.0.1 + seeed-studio/Seeed Arduino rpcUnified @ 2.1.3 + seeed-studio/Seeed_Arduino_mbedtls @ 3.0.1 + seeed-studio/Seeed Arduino RTC @ 2.0.0 + bblanchon/ArduinoJson @ 6.17.3 +build_flags = + -w + -DARDUCAM_SHIELD_V2 + -DOV2640_CAM \ No newline at end of file diff --git a/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/camera.h b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/camera.h new file mode 100644 index 00000000..2028039f --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/camera.h @@ -0,0 +1,160 @@ +#pragma once + +#include +#include + +class Camera +{ +public: + Camera(int format, int image_size) : _arducam(OV2640, PIN_SPI_SS) + { + _format = format; + _image_size = image_size; + } + + bool init() + { + // Reset the CPLD + _arducam.write_reg(0x07, 0x80); + delay(100); + + _arducam.write_reg(0x07, 0x00); + delay(100); + + // Check if the ArduCAM SPI bus is OK + _arducam.write_reg(ARDUCHIP_TEST1, 0x55); + if (_arducam.read_reg(ARDUCHIP_TEST1) != 0x55) + { + return false; + } + + // Change MCU mode + _arducam.set_mode(MCU2LCD_MODE); + + uint8_t vid, pid; + + // Check if the camera module type is OV2640 + _arducam.wrSensorReg8_8(0xff, 0x01); + _arducam.rdSensorReg8_8(OV2640_CHIPID_HIGH, &vid); + _arducam.rdSensorReg8_8(OV2640_CHIPID_LOW, &pid); + if ((vid != 0x26) && ((pid != 0x41) || (pid != 0x42))) + { + return false; + } + + _arducam.set_format(_format); + _arducam.InitCAM(); + _arducam.OV2640_set_JPEG_size(_image_size); + _arducam.OV2640_set_Light_Mode(Auto); + _arducam.OV2640_set_Special_effects(Normal); + delay(1000); + + return true; + } + + void startCapture() + { + _arducam.flush_fifo(); + _arducam.clear_fifo_flag(); + _arducam.start_capture(); + } + + bool captureReady() + { + return _arducam.get_bit(ARDUCHIP_TRIG, CAP_DONE_MASK); + } + + bool readImageToBuffer(byte **buffer, uint32_t &buffer_length) + { + if (!captureReady()) return false; + + // Get the image file length + uint32_t length = _arducam.read_fifo_length(); + buffer_length = length; + + if (length >= MAX_FIFO_SIZE) + { + return false; + } + if (length == 0) + { + return false; + } + + // create the buffer + byte *buf = new byte[length]; + + uint8_t temp = 0, temp_last = 0; + int i = 0; + uint32_t buffer_pos = 0; + bool is_header = false; + + _arducam.CS_LOW(); + _arducam.set_fifo_burst(); + + while (length--) + { + temp_last = temp; + temp = SPI.transfer(0x00); + //Read JPEG data from FIFO + if ((temp == 0xD9) && (temp_last == 0xFF)) //If find the end ,break while, + { + buf[buffer_pos] = temp; + + buffer_pos++; + i++; + + _arducam.CS_HIGH(); + } + if (is_header == true) + { + //Write image data to buffer if not full + if (i < 256) + { + buf[buffer_pos] = temp; + buffer_pos++; + i++; + } + else + { + _arducam.CS_HIGH(); + + i = 0; + buf[buffer_pos] = temp; + + buffer_pos++; + i++; + + _arducam.CS_LOW(); + _arducam.set_fifo_burst(); + } + } + else if ((temp == 0xD8) & (temp_last == 0xFF)) + { + is_header = true; + + buf[buffer_pos] = temp_last; + buffer_pos++; + i++; + + buf[buffer_pos] = temp; + buffer_pos++; + i++; + } + } + + _arducam.clear_fifo_flag(); + + _arducam.set_format(_format); + _arducam.InitCAM(); + _arducam.OV2640_set_JPEG_size(_image_size); + + // return the buffer + *buffer = buf; + } + +private: + ArduCAM _arducam; + int _format; + int _image_size; +}; diff --git a/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/config.h b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/config.h new file mode 100644 index 00000000..ef40b4fa --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/config.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +using namespace std; + +// WiFi credentials +const char *SSID = ""; +const char *PASSWORD = ""; + +const char *PREDICTION_URL = ""; +const char *PREDICTION_KEY = ""; + +// Microsoft Azure DigiCert Global Root G2 global certificate +const char *CERTIFICATE = + "-----BEGIN CERTIFICATE-----\r\n" + "MIIF8zCCBNugAwIBAgIQAueRcfuAIek/4tmDg0xQwDANBgkqhkiG9w0BAQwFADBh\r\n" + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\n" + "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\r\n" + "MjAeFw0yMDA3MjkxMjMwMDBaFw0yNDA2MjcyMzU5NTlaMFkxCzAJBgNVBAYTAlVT\r\n" + "MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKjAoBgNVBAMTIU1pY3Jv\r\n" + "c29mdCBBenVyZSBUTFMgSXNzdWluZyBDQSAwNjCCAiIwDQYJKoZIhvcNAQEBBQAD\r\n" + "ggIPADCCAgoCggIBALVGARl56bx3KBUSGuPc4H5uoNFkFH4e7pvTCxRi4j/+z+Xb\r\n" + "wjEz+5CipDOqjx9/jWjskL5dk7PaQkzItidsAAnDCW1leZBOIi68Lff1bjTeZgMY\r\n" + "iwdRd3Y39b/lcGpiuP2d23W95YHkMMT8IlWosYIX0f4kYb62rphyfnAjYb/4Od99\r\n" + "ThnhlAxGtfvSbXcBVIKCYfZgqRvV+5lReUnd1aNjRYVzPOoifgSx2fRyy1+pO1Uz\r\n" + "aMMNnIOE71bVYW0A1hr19w7kOb0KkJXoALTDDj1ukUEDqQuBfBxReL5mXiu1O7WG\r\n" + "0vltg0VZ/SZzctBsdBlx1BkmWYBW261KZgBivrql5ELTKKd8qgtHcLQA5fl6JB0Q\r\n" + "gs5XDaWehN86Gps5JW8ArjGtjcWAIP+X8CQaWfaCnuRm6Bk/03PQWhgdi84qwA0s\r\n" + "sRfFJwHUPTNSnE8EiGVk2frt0u8PG1pwSQsFuNJfcYIHEv1vOzP7uEOuDydsmCjh\r\n" + "lxuoK2n5/2aVR3BMTu+p4+gl8alXoBycyLmj3J/PUgqD8SL5fTCUegGsdia/Sa60\r\n" + "N2oV7vQ17wjMN+LXa2rjj/b4ZlZgXVojDmAjDwIRdDUujQu0RVsJqFLMzSIHpp2C\r\n" + "Zp7mIoLrySay2YYBu7SiNwL95X6He2kS8eefBBHjzwW/9FxGqry57i71c2cDAgMB\r\n" + "AAGjggGtMIIBqTAdBgNVHQ4EFgQU1cFnOsKjnfR3UltZEjgp5lVou6UwHwYDVR0j\r\n" + "BBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYXjzkwDgYDVR0PAQH/BAQDAgGGMB0GA1Ud\r\n" + "JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMHYG\r\n" + "CCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu\r\n" + "Y29tMEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln\r\n" + "aUNlcnRHbG9iYWxSb290RzIuY3J0MHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6Ly9j\r\n" + "cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5jcmwwN6A1oDOG\r\n" + "MWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5j\r\n" + "cmwwHQYDVR0gBBYwFDAIBgZngQwBAgEwCAYGZ4EMAQICMBAGCSsGAQQBgjcVAQQD\r\n" + "AgEAMA0GCSqGSIb3DQEBDAUAA4IBAQB2oWc93fB8esci/8esixj++N22meiGDjgF\r\n" + "+rA2LUK5IOQOgcUSTGKSqF9lYfAxPjrqPjDCUPHCURv+26ad5P/BYtXtbmtxJWu+\r\n" + "cS5BhMDPPeG3oPZwXRHBJFAkY4O4AF7RIAAUW6EzDflUoDHKv83zOiPfYGcpHc9s\r\n" + "kxAInCedk7QSgXvMARjjOqdakor21DTmNIUotxo8kHv5hwRlGhBJwps6fEVi1Bt0\r\n" + "trpM/3wYxlr473WSPUFZPgP1j519kLpWOJ8z09wxay+Br29irPcBYv0GMXlHqThy\r\n" + "8y4m/HyTQeI2IMvMrQnwqPpY+rLIXyviI2vLoI+4xKE4Rn38ZZ8m\r\n" + "-----END CERTIFICATE-----\r\n"; \ No newline at end of file diff --git a/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/main.cpp b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/main.cpp new file mode 100644 index 00000000..5c3951a1 --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/src/main.cpp @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include "SD/Seeed_SD.h" +#include +#include +#include + +#include "config.h" +#include "camera.h" + +Camera camera = Camera(JPEG, OV2640_640x480); + +WiFiClientSecure client; + +void setupCamera() +{ + pinMode(PIN_SPI_SS, OUTPUT); + digitalWrite(PIN_SPI_SS, HIGH); + + Wire.begin(); + SPI.begin(); + + if (!camera.init()) + { + Serial.println("Error setting up the camera!"); + } +} + +void connectWiFi() +{ + while (WiFi.status() != WL_CONNECTED) + { + Serial.println("Connecting to WiFi.."); + WiFi.begin(SSID, PASSWORD); + delay(500); + } + + client.setCACert(CERTIFICATE); + Serial.println("Connected!"); +} + +void setup() +{ + Serial.begin(9600); + + while (!Serial) + ; // Wait for Serial to be ready + + delay(1000); + + connectWiFi(); + + setupCamera(); + + pinMode(WIO_KEY_C, INPUT_PULLUP); +} + +const float threshold = 0.3f; + +void detectStock(byte *buffer, uint32_t length) +{ + HTTPClient httpClient; + httpClient.begin(client, PREDICTION_URL); + httpClient.addHeader("Content-Type", "application/octet-stream"); + httpClient.addHeader("Prediction-Key", PREDICTION_KEY); + + int httpResponseCode = httpClient.POST(buffer, length); + + if (httpResponseCode == 200) + { + String result = httpClient.getString(); + + DynamicJsonDocument doc(1024); + deserializeJson(doc, result.c_str()); + + JsonObject obj = doc.as(); + JsonArray predictions = obj["predictions"].as(); + + for(JsonVariant prediction : predictions) + { + float probability = prediction["probability"].as(); + if (probability > threshold) + { + String tag = prediction["tagName"].as(); + char buff[32]; + sprintf(buff, "%s:\t%.2f%%", tag.c_str(), probability * 100.0); + Serial.println(buff); + } + } + } + + httpClient.end(); +} + +void buttonPressed() +{ + camera.startCapture(); + + while (!camera.captureReady()) + delay(100); + + Serial.println("Image captured"); + + byte *buffer; + uint32_t length; + + if (camera.readImageToBuffer(&buffer, length)) + { + Serial.print("Image read to buffer with length "); + Serial.println(length); + + detectStock(buffer, length); + + delete (buffer); + } +} + +void loop() +{ + if (digitalRead(WIO_KEY_C) == LOW) + { + buttonPressed(); + delay(2000); + } + + delay(200); +} \ No newline at end of file diff --git a/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/test/README b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/test/README new file mode 100644 index 00000000..b94d0890 --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/code-count/wio-terminal/fruit-quality-detector/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/5-retail/lessons/2-check-stock-device/code-detect/pi/fruit-quality-detector/app.py b/5-retail/lessons/2-check-stock-device/code-detect/pi/fruit-quality-detector/app.py index 3686a971..8c1182fe 100644 --- a/5-retail/lessons/2-check-stock-device/code-detect/pi/fruit-quality-detector/app.py +++ b/5-retail/lessons/2-check-stock-device/code-detect/pi/fruit-quality-detector/app.py @@ -34,7 +34,7 @@ results = predictor.detect_image(project_id, iteration_name, image) threshold = 0.3 -predictions = (prediction for prediction in results.predictions if prediction.probability > threshold) +predictions = list(prediction for prediction in results.predictions if prediction.probability > threshold) for prediction in predictions: print(f'{prediction.tag_name}:\t{prediction.probability * 100:.2f}%') diff --git a/5-retail/lessons/2-check-stock-device/code-detect/virtual-iot-device/fruit-quality-detector/app.py b/5-retail/lessons/2-check-stock-device/code-detect/virtual-iot-device/fruit-quality-detector/app.py index 2c5d3b9d..cc53a73c 100644 --- a/5-retail/lessons/2-check-stock-device/code-detect/virtual-iot-device/fruit-quality-detector/app.py +++ b/5-retail/lessons/2-check-stock-device/code-detect/virtual-iot-device/fruit-quality-detector/app.py @@ -34,7 +34,7 @@ results = predictor.detect_image(project_id, iteration_name, image) threshold = 0.3 -predictions = (prediction for prediction in results.predictions if prediction.probability > threshold) +predictions = list(prediction for prediction in results.predictions if prediction.probability > threshold) for prediction in predictions: print(f'{prediction.tag_name}:\t{prediction.probability * 100:.2f}%') diff --git a/5-retail/lessons/2-check-stock-device/single-board-computer-count-stock.md b/5-retail/lessons/2-check-stock-device/single-board-computer-count-stock.md new file mode 100644 index 00000000..a44ee9d4 --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/single-board-computer-count-stock.md @@ -0,0 +1,161 @@ +# Count stock from your IoT device - Virtual IoT Hardware and Raspberry Pi + +A combination of the predictions and their bounding boxes can be used to count stock in an image + +## Show bounding boxes + +As a helpful debugging step you can not only print out the bounding boxes, but you can also draw them on the image that was written to disk when an image was captured. + +### Task - print the bounding boxes + +1. Ensure the `stock-counter` project is open in VS Code, and the virtual environment is activated if you are using a virtual IoT device. + +1. Change the `print` statement in the `for` loop to the following to print the bounding boxes to the console: + + ```python + print(f'{prediction.tag_name}:\t{prediction.probability * 100:.2f}%\t{prediction.bounding_box}') + ``` + +1. Run the app with the camera pointing at some stock on a shelf. The bounding boxes will be printed to the console, with left, top, width and height values from 0-1. + + ```output + pi@raspberrypi:~/stock-counter $ python3 app.py + tomato paste: 33.42% {'additional_properties': {}, 'left': 0.3455171, 'top': 0.09916268, 'width': 0.14175442, 'height': 0.29405564} + tomato paste: 34.41% {'additional_properties': {}, 'left': 0.48283678, 'top': 0.10242918, 'width': 0.11782813, 'height': 0.27467814} + tomato paste: 31.25% {'additional_properties': {}, 'left': 0.4923783, 'top': 0.35007596, 'width': 0.13668466, 'height': 0.28304994} + tomato paste: 31.05% {'additional_properties': {}, 'left': 0.36416405, 'top': 0.37494493, 'width': 0.14024884, 'height': 0.26880276} + ``` + +### Task - draw bounding boxes on the image + +1. The Pip package [Pillow](https://pypi.org/project/Pillow/) can be used to draw on images. Install this with the following command: + + ```sh + pip3 install pillow + ``` + + If you are using a virtual IoT device, make sure to run this from inside the activated virtual environment. + +1. Add the following import statement to the top of the `app.py` file: + + ```python + from PIL import Image, ImageDraw, ImageColor + ``` + + This imports code needed to edit the image. + +1. Add the following code to the end of the `app.py` file: + + ```python + with Image.open('image.jpg') as im: + draw = ImageDraw.Draw(im) + + for prediction in predictions: + scale_left = prediction.bounding_box.left + scale_top = prediction.bounding_box.top + scale_right = prediction.bounding_box.left + prediction.bounding_box.width + scale_bottom = prediction.bounding_box.top + prediction.bounding_box.height + + left = scale_left * im.width + top = scale_top * im.height + right = scale_right * im.width + bottom = scale_bottom * im.height + + draw.rectangle([left, top, right, bottom], outline=ImageColor.getrgb('red'), width=2) + + im.save('image.jpg') + ``` + + This code opens the image that was saved earlier for editing. It then loops through the predictions getting the bounding boxes, and calculates the bottom right coordinate using the bounding box values from 0-1. These are then converted to image coordinates by multiplying by the relevant dimension of the image. For example, if the left value was 0.5 on an image that was 600 pixels wide, this would convert it to 300 (0.5 x 600 = 300). + + Each bounding box is drawn on the image using a red line. Finally the edited image is saved, overwriting the original image. + +1. Run the app with the camera pointing at some stock on a shelf. You will see the `image.jpg` file in the VS Code explorer, and you will be able to select it to see the bounding boxes. + + ![4 cans of tomato paste with bounding boxes around each can](../../../images/rpi-stock-with-bounding-boxes.jpg) + +## Count stock + +In the image shown above, the bounding boxes have a small overlap. If this overlap was much larger, then the bounding boxes may indicate the same object. To count the objects correctly, you need to ignore boxes with a significant overlap. + +### Task - count stock ignoring overlap + +1. The Pip package [Shapely](https://pypi.org/project/Shapely/) can be used to calculate the intersection. If you are using a Raspberry Pi, you will need to instal a library dependency first: + + ```sh + sudo apt install libgeos-dev + ``` + +1. Install the Shapely Pip package: + + ```sh + pip3 install shapely + ``` + + If you are using a virtual IoT device, make sure to run this from inside the activated virtual environment. + +1. Add the following import statement to the top of the `app.py` file: + + ```python + from shapely.geometry import Polygon + ``` + + This imports code needed to create polygons to calculate overlap. + +1. Above the code that draws the bounding boxes, add the following code: + + ```python + overlap_threshold = 0.20 + ``` + + This defines the percentage overlap allowed before the bounding boxes are considered to be the same object. 0.20 defines a 20% overlap. + +1. To calculate overlap using Shapely, the bounding boxes need to be converted into Shapely polygons. Add the following function to do this: + + ```python + def create_polygon(prediction): + scale_left = prediction.bounding_box.left + scale_top = prediction.bounding_box.top + scale_right = prediction.bounding_box.left + prediction.bounding_box.width + scale_bottom = prediction.bounding_box.top + prediction.bounding_box.height + + return Polygon([(scale_left, scale_top), (scale_right, scale_top), (scale_right, scale_bottom), (scale_left, scale_bottom)]) + ``` + + This creates a polygon using the bounding box of a prediction. + +1. The logic for removing overlapping objects involves comparing all bounding boxes and if any pairs of predictions have bounding boxes that overlap more than the threshold, delete one of the predictions. To compare all the predictions, you compare prediction 1 with 2, 3, 4, etc., then 2 with 3, 4, etc. The following code does this: + + ```python + to_delete = [] + + for i in range(0, len(predictions)): + polygon_1 = create_polygon(predictions[i]) + + for j in range(i+1, len(predictions)): + polygon_2 = create_polygon(predictions[j]) + overlap = polygon_1.intersection(polygon_2).area + + smallest_area = min(polygon_1.area, polygon_2.area) + + if overlap > (overlap_threshold * smallest_area): + to_delete.append(predictions[i]) + break + + for d in to_delete: + predictions.remove(d) + + print(f'Counted {len(predictions)} stock items') + ``` + + The overlap is calculated using the Shapely `Polygon.intersection` method that returns a polygon that has the overlap. The area is then calculated from this polygon. This overlap threshold is not an absolute value, but needs to be a percentage of the bounding box, so the smallest bounding box is found, and the overlap threshold is used to calculate what area the overlap can be to not exceed the percentage overlap threshold of the smallest bounding box. If the overlap exceeds this, the prediction is marked for deletion. + + Once a prediction has been marked for deletion it doesn't need to be checked again, so the inner loop breaks out to check the next prediction. You can't delete items from a list whilst iterating through it, so the bounding boxes that overlap more than the threshold are added to the `to_delete` list, then deleted at the end. + + Finally the stock count is printed to the console. This could then be sent to an IoT service to alert if the stock levels are low. All of this code is before the bounding boxes are drawn, so you will see the stock predictions without overlaps on the generated images. + +1. Run the app with the camera pointing at some stock on a shelf. The output will indicate the number of bounding boxes without overlaps that exceed the threshold. Try adjusting the `overlap_threshold` value to see predictions being ignored. + +> 💁 You can find this code in the [code-count/pi](code-detect/pi) or [code-count/virtual-device](code-detect/virtual-device) folder. + +😀 Your stock counter program was a success! diff --git a/5-retail/lessons/2-check-stock-device/single-board-computer-object-detector.md b/5-retail/lessons/2-check-stock-device/single-board-computer-object-detector.md index 145fa6ad..49760468 100644 --- a/5-retail/lessons/2-check-stock-device/single-board-computer-object-detector.md +++ b/5-retail/lessons/2-check-stock-device/single-board-computer-object-detector.md @@ -43,7 +43,7 @@ The code you used to classify images is very similar to the code to detect objec threshold = 0.3 - predictions = (prediction for prediction in results.predictions if prediction.probability > threshold) + predictions = list(prediction for prediction in results.predictions if prediction.probability > threshold) for prediction in predictions: print(f'{prediction.tag_name}:\t{prediction.probability * 100:.2f}%') diff --git a/5-retail/lessons/2-check-stock-device/wio-terminal-count-stock.md b/5-retail/lessons/2-check-stock-device/wio-terminal-count-stock.md new file mode 100644 index 00000000..35e363b0 --- /dev/null +++ b/5-retail/lessons/2-check-stock-device/wio-terminal-count-stock.md @@ -0,0 +1,3 @@ +# Count stock from your IoT device - Wio Terminal + +Coming soon! \ No newline at end of file diff --git a/images/rpi-stock-with-bounding-boxes.jpg b/images/rpi-stock-with-bounding-boxes.jpg new file mode 100644 index 0000000000000000000000000000000000000000..479170d638f4659ed1037c9ff536d5f326f38d95 GIT binary patch literal 31556 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