{"id":321,"date":"2025-07-23T17:13:56","date_gmt":"2025-07-23T16:13:56","guid":{"rendered":"https:\/\/guillaumesblog.net\/?p=321"},"modified":"2025-08-23T21:48:35","modified_gmt":"2025-08-23T20:48:35","slug":"build-a-serverless-automatic-number-plate-recognition-using-cloud-functions-and-vision-ai","status":"publish","type":"post","link":"https:\/\/guillaumesblog.net\/index.php\/build-a-serverless-automatic-number-plate-recognition-using-cloud-functions-and-vision-ai\/","title":{"rendered":"Build a &#8220;serverless&#8221; Automatic Number Plate Recognition using cloud functions and Vision AI"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Yes &#8211; you read me well; Haven&#8217;t you ever dreamed of being able to monitor which cars park in front of your home? Probably not, but let&#8217;s do it regardless &#8211; and build the system together using the convenience of the cloud.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s have a look at the architecture<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1874\" height=\"881\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/reg_plate_analysis.png\" alt=\"\" class=\"wp-image-325\"\/><\/figure>\n\n\n\n<!--more-->\n\n\n\n<p class=\"wp-block-paragraph\">On the left you&#8217;ve got my home &#8220;on-prem&#8221; with a good old HP PC, a random IP Camera and my Cisco router. I share a couple of videos of my setup below &#8211; nothing too exiting \ud83d\ude09<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>\u26a0\ufe0f <strong>btw:<\/strong> This is a home lab experiment \u2014 not production-ready.<br>Tell me how and please share your thoughts in the comments.<\/em><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1600\" height=\"739\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/20250722_181136.jpg\" alt=\"\" class=\"wp-image-329\"\/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1600\" height=\"739\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/20250723_145332.jpg\" alt=\"\" class=\"wp-image-328\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Right, first thing we do is to retrieve the video stream from the IP camera. There is a bit of fiddle with the firmware to assign an IP, launch the RTSP protocol and select the stream quality but it is not too challenging. Anyway this is out of topic  and I won&#8217;t describe that in this article.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now let&#8217;s go to the HP computer, the &#8220;Stream process machine&#8221; on debian 12, and write this python script below<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import cv2\nimport os\nfrom datetime import datetime\n\n<strong>cap = cv2.VideoCapture(\"rtsp:\/\/usr:passwd@192.168.1.200:554\/h264Preview_01_main\")<\/strong>\nw = cap.get(cv2.CAP_PROP_FRAME_WIDTH)\nh = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)\nfps = cap.get(cv2.CAP_PROP_FPS) \n\nprint(f\"Resolution:{w}x{h}, FPS: {fps}\")\n\nsaved_frame_count = 0\nframe_count = 0\nsuccess,image= cap.read()\n\nwhile success:\n    if frame_count % 2 == 0:\n        ts = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n        filename= f\"frame_{ts}.jpg\"\n        <strong>cv2.imwrite(f\"\/home\/guillaume\/ram_videos\/{filename}\", image)<\/strong>\n        print(f\"Saved a new frame '{filename}' (# {saved_frame_count} ), Success={success}\")\n        saved_frame_count +=1\n\n\n    frame_count += 1\n    success,image = cap.read()\n\ncap.release()\ncv2.destroyAllWindows()<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">We use the cv2 library to capture and process the stream, we take one frame per second and save it into a folder <em>cv2.imwrite(f&#8221;\/home\/guillaume\/ram_videos\/<\/em> which is btw a folder mounted in memory with tmpfs; Earlier I did save the pictures straight on my HDD but my computer would lag badly (likely the OS gets overwhelmed with IO)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We&#8217;ve got our pictures saved and added continuously to the RAM folder; now we want to flush them every 60 seconds from RAM to HDD, so we&#8217;ve got another script, this time using bash moving the files across like so<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\nRAM_DIR=\/home\/guillaume\/ram_videos\nHDD_DIR=\/home\/guillaume\/videos\n\nif &#91;&#91; -d \"$RAM_DIR\" &amp;&amp; -d \"$HDD_DIR\" ]];then\n\techo \" Source and target folder exist. OK.\"\nelse\n\techo \"Error: One or both folders are missing, please check.\"\n\texit 1\nfi\n\n\nwhile true; do\n\tfile_count=$(ls -1 \"$RAM_DIR\" | wc -l)\n\tif &#91; \"$file_count\" -ge 60 ]; then\n\t\techo \"$(date): Copying $file_count files to HDD at $HDD_DIR from $RAM_DIR...\"\n\n\t\t<strong>for f in \"$RAM_DIR\"\/*; do\n\t\t\tcp \"$f\" \"$HDD_DIR\"\/ &amp;&amp; rm \"$f\"<\/strong>\n\t\tdone\n\n\t\techo \"Done.\"\n\n\tfi\n\tsleep 20\ndone<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Next we are going to send the HDD videos to OCI via our VPN tunnel using the object storage private endpoint using an infinite loop, again using another bash script. Don&#8217;t forget to add the endpoint to the \/etc\/hosts file so the requests goes via your VPN and not via Internet. For a lab internet would be OK though, I have got another article <a href=\"https:\/\/guillaumesblog.net\/index.php\/hybrid-cloud-home-lab-to-oracle-oci\/\">here<\/a> that explains how to set-up a VPN connection between On-prem and OCI.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\nHDD_DIR=\/home\/guillaume\/videos\n\nwhile true; do\n\tif &#91; -n \"$(find \"$HDD_DIR\" -maxdepth 1 -type f)\" ]; then\n\n\t\tfor f in \"$HDD_DIR\"\/*; do\n\t\t\t<strong>oci os object put --bucket-name lab_videos --file \"$f\" --endpoint https:\/\/&lt;your-private-endpoint-dns-prefix&gt;-&lt;your-namespace&gt;.private.objectstorage.&lt;region&gt;.oci.customer-oci.com<\/strong>\n\t\t\tif &#91; $? -eq 0 ]; then\n\t\t\t\techo \"Uploaded: $f\"\n\t\t\telse\n\t\t\t\techo \"Failed to upload $f to OCI\"\n\t\t\tfi\n\t\tdone\n\t\techo \"Done uploading to OCI.\"\n\tfi\n\tsleep 10\ndone<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I run the three scripts in parallel like below; the python script handling the video stream is on the top left, the bash copy from RAM to HDD is top right and the bash copy from HDD to OCI is bottom left.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/reg_8.png\" alt=\"\" class=\"wp-image-333\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">We are now done with our on-premise set-up, our computer is sending out a picture\/second out to OCI object storage in a bucket called lab_videos; in case you&#8217;ve missed it it is written in the code above.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s look at the right hand side part of the architecture, the &#8220;cloud&#8221; side. See the bucket getting tons of frames in; we have turned &#8220;Emit Object Events&#8221; on.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1359\" height=\"905\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/bucket0.png\" alt=\"\" class=\"wp-image-346\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">OCI Events needs to react to any file uploaded to the bucket, that&#8217;s why we have &#8220;Emit Object Events&#8221; in a way that it needs to trigger our future function based on a rule<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1768\" height=\"907\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/reg10.png\" alt=\"\" class=\"wp-image-345\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s now look at the function that processes a frame, detects the number plates as soon as a frame is added to my bucket, and then stores it in a database. That was the most challenging part, as it requires a bit of reading through the OCI Python SDK <a href=\"https:\/\/docs.oracle.com\/en-us\/iaas\/tools\/python\/2.155.2\/index.html\">https:\/\/docs.oracle.com\/en-us\/iaas\/tools\/python\/2.155.2\/index.html<\/a> and a tiny bit of programming experience &#8211; our best friend LLM comes in super handy indeed&#8230; the code is in my github <a href=\"https:\/\/github.com\/geddegda\/plate-recognition\/tree\/main\">here<\/a>. Beware that the function is made of 3 files func.py, func.yaml and requirements.txt; if you are familiar with how docker works you probably know that already.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What we want to do then is to package this function and to push it into OCI function &#8211; I use the OCI cloud editor for this, which gives me a combo of coding environment and a cloud shell, it is handy so I don&#8217;t have to work locally, see below<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1911\" height=\"943\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/reg_7.png\" alt=\"\" class=\"wp-image-342\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Once your function is ready and has been deployed, it will now appear under Applications in OCI Functions &#8211; and for your function to be able to call on other services e.g. to pick up a file in object storage, AI vision to analyse a frame and NoSQL to write a new record in you need to give it the correct policies to the respective services &#8211; this procedure is called <a href=\"https:\/\/docs.oracle.com\/en-us\/iaas\/Content\/Identity\/Tasks\/callingservicesfrominstances.htm\">instance principal<\/a> ; <strong>Because this is a lab<\/strong>, I give it an <strong>over permissive<\/strong> policy.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>allow dynamic-group function-reg-plate to manage all-resources in tenancy<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Your function is now in Applications and OCIR, we see the first calls coming in, and its image being stored in the Oracle Image Registry.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1909\" height=\"892\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/reg_1.png\" alt=\"\" class=\"wp-image-336\"\/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1906\" height=\"942\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/reg_6.png\" alt=\"\" class=\"wp-image-341\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Now let&#8217;s have a look, if I go on the AI vision console and put the URI of an image stored my bucket manually what type of answer do I get?<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1909\" height=\"937\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/08\/reg_5.png\" alt=\"\" class=\"wp-image-367\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">I would see these two number plates. (These are my vehicles, so no need to get too exited.) Let&#8217;s see if my function is able to catch then automatically, let&#8217;s look at the logs my function is producing.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1891\" height=\"939\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/reg_3.png\" alt=\"\" class=\"wp-image-338\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Yes, they appear! It means my function works and it is all automated; and by the way there is a very convenient way for your print in Python to show in OCI Logging is to add <em>flush=True<\/em>, you can have a look at my code again.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now let&#8217;s look at the final step; do I have the timestamp and the details of the vehicules driving in? Let&#8217;s open the database records..<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1882\" height=\"894\" src=\"https:\/\/guillaumesblog.net\/wp-content\/uploads\/2025\/07\/reg_4.png\" alt=\"\" class=\"wp-image-339\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Boom! It works; we now have a complete Automatic Number Plate Recognition record system at home for absolutely no reason whatsoever! What I like is the convenience and the ease of use, as it give us so much power to come up with quick solutions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Looking forward we could imagine a way to send out automated SMS or emails out to the owners, as long as we have the owners data indeed, in this case myself \ud83d\ude09 and also add some logic to allow parking per blocks of 8 hours, so we don&#8217;t insert a record in the database everytime a frame is uploaded&#8230; But in the meantime this is the end of this article so see you next time!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Yes &#8211; you read me well; Haven&#8217;t you ever dreamed of being able to monitor which cars park in front of your home? Probably not, but let&#8217;s do it regardless &#8211; and build the system together using the convenience of the cloud. Let&#8217;s have a look at the architecture<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-321","post","type-post","status-publish","format-standard","hentry","category-conversation"],"_links":{"self":[{"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/posts\/321","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/comments?post=321"}],"version-history":[{"count":25,"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/posts\/321\/revisions"}],"predecessor-version":[{"id":376,"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/posts\/321\/revisions\/376"}],"wp:attachment":[{"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/media?parent=321"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/categories?post=321"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/guillaumesblog.net\/index.php\/wp-json\/wp\/v2\/tags?post=321"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}