Guide: Local License Plate Recognition With Home Assistant, CodeProject.AI, and Frigate

I spent quite a while trying different ideas to get a working & fully local license plate setup and I am quite happy with this one so I am sharing a guide on how to do this.

Prerequisites

  • You must have CodeProject.AI setup with the license plate plugin installed, that is outside the scope of this guide.
  • Frigate+ is recommended for its built in license plate detection, however this can definitely work without Frigate+ with some modifications that are called out in the comments

Preliminary Info

The approach in this guide requires using a python script to run some of the logic because it would be much more difficult to do in a Home Assistant automation. This script is called directly from home assistant.

Python Script

  1. Create a folder to hold your scripts (if you don’t already have one) inside of the Home Assistant /config directory. For this guide I will call mine /config/bashscript
  2. Create a file called license_plate.py in /config/bashscript

The contents of the file should be

import datetime
import io
import json
import requests
import sys

from PIL import Image

# host for frigate instance
FRIGATE_HOST = "http://10.0.0.0:5000"

# host for CodeProject.AI instance
CODE_PROJECT_HOST = "http://10.0.0.0:4100"

event_id = sys.argv[1]
json = requests.get(f"{FRIGATE_HOST}/api/events/{event_id}").json()
camera = json["camera"]

# get clean snapshot if event has bended otherwise get snapshot without bounding boxes
if json["end_time"]:
    img_bytes = requests.get(f"{FRIGATE_HOST}/clips/{camera}-{event_id}-clean.png").content
else:
    img_bytes = requests.get(f"{FRIGATE_HOST}/api/events/{event_id}/snapshot.jpg?bbox=0").content

img = Image.open(io.BytesIO(img_bytes))

attributes = json["data"]["attributes"]

# Checks to ensure that license plate is detected
# this will need to be removed if not using a model that detects license plates (like frigate+)
if not attributes:
    with open("/config/bashscript/detected_plates.log", "a") as log:
        log.write(f"[{datetime.datetime.now()}]: {camera} - could not find license plate in {json}\n")
		exit(0)

box = attributes[0]["box"]

detect = requests.get(f"{FRIGATE_HOST}/api/config").json()["cameras"][camera]["detect"]
known_res = (detect["width"], detect["height"])

# find coordinates of image to send to detector
x_mid = (box[0] + (box[2] / 2)) * known_res[0]
y_max = (box[1] + box[3]) * known_res[1]

cropped_image = img.crop((max(0, x_mid - 150), 0, min(known_res[0], x_mid + 150), known_res[1]))
cropped_image.save("/config/bashscript/crop.jpg")

# you will likely need to create multiple iterations for each vehicle
# 0/8/B for example are often mixed up
known_plates = {
    # Bob's Car
    "ABC128": "Bob's Car",
    "ABC12B": "Bob's Car",
    
    # Steve's Truck
    "123TR0": "Steve's Truck",
    "123TRO": "Steve's Truck",
}

with open('/config/bashscript/crop.jpg', 'rb') as fp:
    response = requests.post(
        f'{CODE_PROJECT_HOST}/v1/image/alpr',
        files=dict(upload=fp),
    )
    print(response.json())
    plates = response.json()
    plate = None

    if len(plates["predictions"]) > 0 and plates["predictions"][0].get("plate"):
        plate = str(plates["predictions"][0]["plate"]).replace(" ", "")
        score = plates["predictions"][0]["confidence"]
        print(f"Checking plate: {plate} in {known_plates.keys()}")

        with open("/config/bashscript/detected_plates.log", "a") as log:
            log.write(f"[{datetime.datetime.now()}]: {camera} - detected {plate} as {known_plates.get(plate)} with a score of {score}\n")

        if plate in known_plates.keys():
            print(f"{camera} - Found a known plate: {known_plates[plate]}")
        else:
            plate = None
    else:
        with open("/config/bashscript/detected_plates.log", "a") as log:
            log.write(f"[{datetime.datetime.now()}]: {camera} - No plates detected in run: {plates}\n")

    if plate is None:
        print(f"No valid results found: {plates['predictions']}")
        sys.exit()

vehicle_name = known_plates[plate]

# Add Sub Label To Car Event
requests.post(f"{FRIGATE_HOST}/api/events/{event_id}/sub_label", json={"subLabel": vehicle_name, "subLabelScore": round(score, 2)})

There are a few things to note here:

  • CodeProject.AI port might be different depending on where and how it is setup
  • Some characters are commonly mixed up like 0/O and 8/B, these variations should be added so this works more reliably.
  • This will create a file detected_plates.log which will provide info on what plates are detected and if it matches a known plate
  • The latest image sent to ALPR will be saved as /config/bashscript/crop.jpg

Home Assistant Configuration

The next step is adding this python script to the Home Assistant configuration.yaml file. This is done via the shell_command type:

shell_command:
  # run license plate inference for vehicle
  check_license_plate: python3 /config/bashscript/license_plate.py "{{ event_id }}"

Home Assistant Automation

The final step is setting up the Home Assistant automation, this will run on a car event only when it has a license plate attribute detected. It will then run 3 iterations of checking for the license plate and stop when a license plate is detected.

This single automation will run on all cameras that detect a car with a license plate in the zones defined, so if you have a driveway zone specified for the garage and driveway camera it will run on both.

alias: >-
  Outside - Driveway - Frigate Try To Read License Plate - License Plate
  Seen
description: ""
trigger:
  - platform: mqtt
    topic: frigate/events
    id: frigate-event
    payload: license_plate
    # will just want to check that the label == "car" if not using a model that supports license_plate
    value_template: "{{ value_json[\"after\"][\"current_attributes\"][0]['label'] }}"
    variables:
      after_zones: "{{ trigger.payload_json[\"after\"][\"entered_zones\"] }}"
      before_zones: "{{ trigger.payload_json[\"before\"][\"entered_zones\"] }}"
      current_zones: "{{ trigger.payload_json[\"after\"][\"current_zones\"] }}"
      camera: "{{ trigger.payload_json[\"after\"][\"camera\"] }}"
      id: "{{ trigger.payload_json[\"after\"][\"id\"] }}"
      label: "{{ trigger.payload_json[\"after\"][\"label\"] }}"
condition:
  - condition: and
    conditions:
      # setup zones that you want to look for cars with license plates for detection
      - condition: template
        value_template: >-
          {{ ["street", "driveway"] | select("in", after_zones) | list | length
          > 0 }}
        alias: Has been in the street
      - condition: template
        value_template: "{{ [\"driveway\"] | select(\"in\", current_zones) | list | length > 0 }}"
    alias: Was in the street and is now in the driveway
  - condition: template
    value_template: "{{ trigger.payload_json['after']['sub_label'] is none }}"
action:
  - choose:
      - conditions:
          - condition: trigger
            id: frigate-event
        sequence:
          - service: shell_command.check_license_plate
            data_template:
              event_id: "{{id}}"
              camera: "{{camera}}"
          - repeat:
              count: 2
              sequence:
                - wait_for_trigger:
                    - platform: mqtt
                      topic: frigate/events
                      payload: "{{ trigger.payload_json['after']['id'] }}"
                      value_template: "{{ value_json['after']['id'] }}"
                  timeout:
                    hours: 0
                    minutes: 10
                    seconds: 0
                    milliseconds: 0
                  continue_on_timeout: false
                - condition: template
                  value_template: >-
                    {{ wait.trigger.payload_json['after']['sub_label'] is none
                    }}
                - service: shell_command.check_license_plate
                  data_template:
                    event_id: "{{id}}"
                    camera: "{{camera}}"
    default: []
trace:
  stored_traces: 20
mode: parallel
max: 10

8 Likes

H i try setting this up but I have some questions:

  1. i use one cam for driveway and another for street. Does this work?
  2. will this create entities or how would i use it?
1 Like

Hi, sir .I’m an LPR camera engineer and good at java software code . You can contact me by following the link . I can offer you some code , SDK ,API and ANPR Camera software system

Does the Frigate+ model return the actual license plate characters? Or just Identifies that there is a license plate there?

The Frigate+ model identifies plates and then a separate service (in this case CodeProject.AI) uses the image that is cropped on that area that frigate detected a plate and reads it

Got it, any plans to make this an addon or some easier to deploy/use method?

4 Likes