Combining models face and age - no age displayed

Hello together,

first of all, thank you for developing and sharing such a great app, it makes using hailo so much easier!

Actually I am working on a project on a PI5 with AI Hailo hat, connected via MQTT with home assistant, nearly everything works fine, I get the stream with overlays in HA and could change the models on the fly via MQTT, but the combining face and age shows "Age" but there will be no age displayed, the same with face and emotion.

face and gender works fine.

used models:
“yolov8n_relu6_face–640x640_quant_hailort_hailo8_1”
“yolov8n_relu6_age–256x256_quant_hailort_hailo8_1”

“yolov8n_relu6_face–640x640_quant_hailort_hailo8_1”
emotion_recognition_fer2013–64x64_quant_hailort_multidevice_1"

using:

compound = degirum_tools.CroppingAndClassifyingCompoundModel(
                det_model, cls_model, crop_extent
            )
            return {
                det_cfg["result_key"]: det_model,
                cls_cfg["result_key"]: cls_model,
                "compound": compound
            }

I have a csi connected camera using picamera2, and all single models working fine as well as the combined models face and gender.
Using a local model-zoo

Hi @core-stuff, appreciate your words about PySDK. One of our goals is in fact to make building inference pipelines easier.

Quick question, in your code, do you have self._show_probabilities set to True?

I see you’re using three classifying/detection models and one age model. Age estimation models store their result in the score field rather than label field. By default, the model’s property overlay_show_probabilities is set to False.

1 Like

thank you for this hint, if i run the age model in single mode i get displayed the age but the combination crashes, but it‘s a step forward.

with this helper

def fix_age_labels(results):
    for r in results:
        if r.get("label", "").lower() == "age":
            r["label"] = f"Age: {r.get('score', 0):.0f}y"
    return results

and that call

fix_age_labels(result.results)

I got it displayed.

Hi @core-stuff,

you might be able to do what you want entirely with PySDK and DeGirum Tools and no additional helpers:

# Declaring variables
hw_location = "@cloud"
face_model_zoo_url = "degirum/hailo"
face_model_name = "yolov8n_relu6_face--640x640_quant_hailort_hailo8_1"
age_model_zoo_url = "degirum/hailo"
age_model_name = "yolov8n_relu6_age--256x256_quant_hailort_hailo8_1"
video_source = "https://raw.githubusercontent.com/DeGirum/PySDKExamples/main/images/faces_and_gender.mp4"

# The face and age cropping compound model code
import degirum as dg
import degirum_tools

# Connect to AI inference engine
face_zoo = dg.connect(hw_location, face_model_zoo_url, degirum_tools.get_token())
age_zoo = dg.connect(hw_location, age_model_zoo_url, degirum_tools.get_token())

# Load models
with face_zoo.load_model(face_model_name, overlay_show_probabilities = True, overlay_show_labels=True, overlay_line_width=1, overlay_font_scale=2) as face_model:
    with age_zoo.load_model(age_model_name) as age_model:
        # Create a compound cropping model with 50% crop extent
        crop_model = degirum_tools.CroppingAndClassifyingCompoundModel(
            face_model, age_model, 50.0
        )
        inference_results = degirum_tools.predict_stream(crop_model, video_source)
        
        # Detect faces and ages
        with degirum_tools.Display("Age Estimation", show_fps=False) as display:
            for inference_result in inference_results:
                display.show(inference_result)

Would this code serve your purposes?

Thank you for your answers and the great support , in that case, i will give it a try.

1 Like

@raytao-degirum there is one thing I am struggling: the face_and_age compound is the only model where I have had the problems to get the result of the second model displayed, face_and_gender work as well as face_and_emotion, with my actually code.

there is the model_loader where I load the models and create the command:

import yaml
import degirum as dg
import degirum_tools


class ModelLoader:
    def __init__(
        self,
        config_path="<path>degirum-zoo/models.yaml",
        zoo_path="<path>degirum-zoo",
        inference_host="@local",
        token=""
    ):
        self.zoo_path = zoo_path
        self.inference_host = inference_host
        self.token = token

        with open(config_path, "r") as f:
            self.config = yaml.safe_load(f)["models"]

    def load_model(self, model_name):
        if model_name not in self.config:
            raise ValueError(f"model '{model_name}' not found in config.")

        model_cfg = self.config[model_name]
        model_type = model_cfg.get("type", "single")

        if model_type == "single":
            return self._load_single(model_cfg)

        elif model_type == "compound":
            return self._load_compound(model_cfg)

        else:
            raise ValueError(f"Unknown model type: {model_type}")

    def configure_model_overlay(self, model, config):
        for key, attr in [
            ("overlay_show_labels", "overlay_show_labels"),
            ("overlay_show_probabilities", "overlay_show_probabilities"),
            ("overlay_font_scale", "overlay_font_scale"),
            ("overlay_line_width", "overlay_line_width"),
            ("overlay_alpha", "overlay_alpha"),
            ("overlay_blur", "overlay_blur"),
        ]:
            if key in config:
                setattr(model, attr, config[key])

        if "overlay_font_color" in config:
            try:
                raw_color = config["overlay_font_color"]
                overlay_font_color = [tuple(raw_color)]
                model.overlay_font_color = overlay_font_color
                print("Overlay-font-color set successfully:", overlay_font_color)
            except Exception as e:
                print(f"Error setting overlay_font_color: {e}")

        if "overlay_color" in config:
            try:
                raw_color = config["overlay_color"]
                if isinstance(raw_color[0], list):  # multible colors
                    overlay_color = [tuple(c) for c in raw_color]
                else:  # single color
                    overlay_color = [tuple(raw_color)]
                model.overlay_color = overlay_color
                print("Overlay-color set successfully:", overlay_color)
            except Exception as e:
                print(f"Error setting overlay_color: {e}") 

    # load single model
    def _load_single(self, model_cfg):
        model = dg.load_model(
            model_name=model_cfg["path"],
            inference_host_address=self.inference_host,
            zoo_url=self.zoo_path,
            token=self.token
        )

        self.configure_model_overlay(model, model_cfg)

        result_key = model_cfg.get("result_key", "default")
        return {result_key: model}
    
    # load compound model
    def _load_compound(self, model_cfg):
        components = model_cfg.get("components", [])
        if len(components) != 2:
            raise ValueError("Compound models require exactly 2 components.")

        strategy = model_cfg.get("strategy", "crop_classify")
        crop_extent = model_cfg.get("crop_extent", 30.0)

        # Load components
        det_cfg = components[0]
        cls_cfg = components[1]

        det_model = dg.load_model(
            model_name=det_cfg["path"],
            inference_host_address=self.inference_host,
            zoo_url=self.zoo_path,
            token=self.token
        )

        self.configure_model_overlay(det_model, det_cfg)

        cls_model = dg.load_model(
            model_name=cls_cfg["path"],
            inference_host_address=self.inference_host,
            zoo_url=self.zoo_path,
            token=self.token
        )

        self.configure_model_overlay(cls_model, cls_cfg)

        # Strategy: cropping and classifying
        if strategy == "crop_classify":
            
            compound = degirum_tools.CroppingAndClassifyingCompoundModel(
                det_model, cls_model, crop_extent
            )
            return {
                det_cfg["result_key"]: det_model,
                cls_cfg["result_key"]: cls_model,
                "compound": compound
            }

        # Strategy: chaining (you take care of the interaction)
        elif strategy == "chain":
            return {
                det_cfg["result_key"]: det_model,
                cls_cfg["result_key"]: cls_model
            }

        # Strategy: parallel (both models run in parallel and you take care of the interaction)
        elif strategy == "parallel":
            return {
                det_cfg["result_key"]: det_model,
                cls_cfg["result_key"]: cls_model
            }

        else:
            raise NotImplementedError(f"Strategy '{strategy}' is not supported.")

and then there is the routine in the main.py where the results are dislpayed (prepared for streaming to Homeassistant)

...
# initialize camera
picam2 = Picamera2()
picam2.start()

def fix_age_labels(results):
    for r in results:
        if r.get("label", "").lower() == "age":
            r["label"] = f"Age: {r.get('score', 0):.0f}y"
    return results

def run_inference(frame):
    global current_model

    if not current_model:
        return frame  #if no model is loaded, return the original frame

    # single model
    if callable(current_model):
        result = current_model(frame)
        return result.image_overlay

    # compoundmodell (as dict)
    elif isinstance(current_model, dict):

        if "compound" in current_model:
            result = current_model["compound"](frame)
            
            if model_name == "face_and_age":
                fix_age_labels(result.results)

            return result.image_overlay

        # parallel
        else:
            overlay = frame.copy()
            for model in current_model.values():
                result = model(frame)
                overlay = result.image_overlay
            return overlay

    # Fallback
    return frame

app = Flask(__name__)

def gen_frames():
    while True:
        frame = picam2.capture_array()
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

        processed_frame = run_inference(frame_rgb)

        ret, jpeg = cv2.imencode('.jpg', processed_frame)
        if not ret:
            continue
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + jpeg.tobytes() + b'\r\n')



@app.route('/video_feed')
def video_feed():
    return Response(gen_frames(),
                    mimetype='multipart/x-mixed-replace; boundary=frame')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, threaded=True)

Hi @core-stuff,

Have you enabled overlay_show_probabilities for the first model used in your compound model?

This will display only the word “Age”:

    det_model = inference_manager.load_model("yolov8n_relu6_face--640x640_quant_hailort_multidevice_1")
    cls_model = inference_manager.load_model("yolov8n_relu6_age--256x256_quant_hailort_hailo8l_1", overlay_show_probabilities=True)

Whereas this will display the word “Age” followed by the score:

    det_model = inference_manager.load_model("yolov8n_relu6_face--640x640_quant_hailort_multidevice_1", overlay_show_probabilities=True)
    cls_model = inference_manager.load_model("yolov8n_relu6_age--256x256_quant_hailort_hailo8l_1")

The reason for this is twofold:

  1. Compound models patch inference results of the first model with the results of the second model.
  2. The overlay is drawn based on the inference results of the first model.

As an example, the following code produce these InferenceResults for one frame:

- bbox: [465, 320, 491, 374]
  category_id: 0
  label: Age
  score: 36.499188025792435
"""
Loads a detection model and a classification model from a model zoo using
``degirum_tools``. The two models are combined into a
`degirum_tools.compound_models.CroppingAndClassifyingCompoundModel` and
applied to frames on video_source at index 1.

Requires environment variable DEGIRUM_CLOUD_TOKEN.
Requires environment variable CLOUD_ZOO_URL to be set to degirum/hailo.
"""
from __future__ import annotations

from degirum_tools.inference_support import (
    connect_model_zoo,
    CloudInference,
    predict_stream,
)
from degirum_tools.compound_models import (
    CroppingAndClassifyingCompoundModel,
)
from degirum_tools.video_support import open_video_stream, video_source
from degirum_tools import Display

def main() -> None:
    """Run inference with a compound model."""
    inference_manager = connect_model_zoo(CloudInference)
    det_model = inference_manager.load_model(
        "yolov8n_relu6_face--640x640_quant_hailort_multidevice_1",
        overlay_show_probabilities=True,
    )
    cls_model = inference_manager.load_model(
        "yolov8n_relu6_age--256x256_quant_hailort_hailo8l_1"
    )
    compound = CroppingAndClassifyingCompoundModel(det_model, cls_model, 30.0)

    try:
        with Display("Compound Demo") as display:
            for result in predict_stream(compound, 1):
                print(result)
                display.show(result.image_overlay)
    except KeyboardInterrupt:
        pass

if __name__ == "__main__":
    main()

To debug your code, try printing the InferenceResult and seeing what comes out.

2 Likes

@raytao-degirum Thank you so much for pointing me to my mistake, I have had disable doverlay_show_probabilities for the first model that, as you explained, only showed “Age”. Now it works perfect without the helper function :+1: :folded_hands: :sparkles:

1 Like

No problem, and feel free to ask any other questions.

Discussions like this help other individuals who are building workflows with PySDK and DeGirum Tools. Future users Googling or ChatGPTing for solutions would find this thread useful to learn about unique aspects of CompoundModels, like the patching behavior I described above.

1 Like