Practical case: DeepStream defect detection in manufacturing

Practical case: DeepStream defect detection in manufacturing — hero

Objective and use case

What you’ll build: You will build a real-time conveyor defect detection system on an NVIDIA Jetson Orin Nano Developer Kit using DeepStream for GPU-accelerated vision, an Arducam IMX477 CSI camera as the sensor, and an Adafruit PCA9685 PWM driver to actuate a diverter servo when a defective item is detected.

Why it matters / Use cases

  • Inline QA for manufacturing: detect missing labels, color-marked rejects, and size anomalies before packaging; trigger a pneumatic gate or servo to divert defects.
  • Food processing: identify contaminants (e.g., red stickers/tagged items indicating potential contamination) and separate them to a quarantine bin.
  • Logistics sorting: detect damaged parcels (e.g., torn corners indicated with a red tag) and route to manual inspection.
  • Pharmaceutical lines: monitor blister packs for missing pills and mark trays as reject.
  • Electronics assembly: identify wrong-component placements or missing screws by color-coding flagged assemblies and automatically stopping the line.

Expected outcome

  • Achieve ≥ 95% accuracy in defect detection.
  • Reduce false positives to less than 2% in quality assurance checks.
  • Improve processing speed to handle ≥ 100 items per minute on the conveyor.
  • Decrease manual inspection time by 30% through automated defect identification.
  • Maintain system latency below 200 milliseconds for real-time processing.

Audience: Manufacturing engineers; Level: Intermediate

Architecture/flow:

Prerequisites

  • Platform: NVIDIA Jetson Orin Nano Developer Kit with JetPack (L4T) on Ubuntu.
  • Camera: Arducam IMX477 MIPI Camera (Sony IMX477) connected to CSI.
  • PWM/Actuator: Adafruit 16-Channel PWM Driver (PCA9685) driving a 5V hobby servo (e.g., SG90/MG996R).
  • Internet connectivity for installing packages.
  • Terminal-only flow (no GUI required). Optional: HDMI monitor to visualize OSD.

Verify JetPack, kernel, and NVIDIA packages:

cat /etc/nv_tegra_release

uname -a
dpkg -l | grep -E 'nvidia|tensorrt'

Recommended stack versions for this case:
– JetPack/L4T 35.4.1 (JetPack 5.1.2).
– DeepStream 6.3 (Jetson).
– Python 3.8.
– pyds DeepStream Python bindings (wheel shipped with DeepStream).

Materials (with exact model)

  • NVIDIA Jetson Orin Nano Developer Kit.
  • Arducam IMX477 MIPI Camera (Sony IMX477).
  • Adafruit 16-Channel PWM Driver (PCA9685), default I2C address 0x40.
  • 5V hobby servo (standard 3-wire, signal at 3.3V logic tolerant, powered from 5–6V rail).
  • External 5V DC supply for servos (do NOT power servos from Jetson 5V header).
  • Dupont wires; common ground between Jetson and external 5V supply.

Setup/Connection

Electrical connections (text and table)

  • Use Jetson 40-pin header I2C bus 1 (pins 3=SDA1, 5=SCL1). Power PCA9685 VCC from Jetson 3.3V (pin 1). Power servo rail (V+) from an external, adequately rated 5V supply. Tie grounds together.
Function Jetson Orin Nano 40-pin PCA9685 board Servo (CH0) Notes
I2C SDA Pin 3 (I2C1 SDA) SDA 3.3V logic
I2C SCL Pin 5 (I2C1 SCL) SCL 3.3V logic
3.3V logic power Pin 1 (3V3) VCC Logic only, not servo power
Ground Pin 6 (GND) GND GND (brown/black) Common ground with 5V supply
5V servo power External 5V V+ V+ (red) External PSU (≥2A depending on servo)
Servo signal CH0 SIG Signal (orange/white) PWM from PCA9685 CH0

Safety notes:
– Never power the servo from Jetson’s 5V pin. Use an external 5V PSU.
– Always share ground between Jetson GND and external PSU GND.
– Keep wire runs short; servos inject noise—decouple V+ with electrolytic (≥470 µF).

Enable and verify I2C

# Install I2C tools
sudo apt update
sudo apt install -y i2c-tools

# Add your user to i2c group (log out/in afterwards)
sudo usermod -aG i2c $USER

# Discover PCA9685 (default 0x40) on I2C bus 1
i2cdetect -y -r 1
# Expect to see "40" in the matrix. If not, check wiring and power.

Camera setup and sanity test

  • Ensure the IMX477 is firmly seated on CSI, and the ribbon cable is oriented correctly.
  • If you installed Arducam’s IMX477 driver or overlays for JetPack 5.x, reboot after installation.
  • Test with GStreamer Argus source:
# Basic camera test without display (headless)
gst-launch-1.0 nvarguscamerasrc \
  ! 'video/x-raw(memory:NVMM),width=1920,height=1080,framerate=30/1,format=NV12' \
  ! nvvideoconvert ! 'video/x-raw,format=I420' ! fakesink -v

If you see caps negotiation and buffers flowing, the camera is working. If you see “No cameras available” or nvargus errors, check IMX477 driver status and cabling.

Full Code

We’ll implement a DeepStream Python pipeline with:
– Source: nvarguscamerasrc (IMX477, 1080p30).
– nvstreammux (batch-size=1).
– nvinfer (resnet10 detector FP16; shipped with DeepStream).
– A pad probe to:
– Extract object metadata.
– Sample a small region of the frame inside each object and compute red pixel fraction (defect if red_fraction > threshold).
– Overlay “DEFECT” and trigger PCA9685 servo to 90° briefly, otherwise keep at 0°.
– Sink: fakesink (headless).

We use the DeepStream resnet10 Primary_Detector model and a custom nvinfer config.

Create a working directory:

mkdir -p ~/deepstream-conveyor/models
cd ~/deepstream-conveyor

Create pgie configuration file pgie_conveyor.txt:

# File: pgie_conveyor.txt
[property]
gpu-id=0
net-scale-factor=0.003921569790691
model-color-format=0
# Caffe resnet10 Primary Detector shipped with DS
labelfile-path=/opt/nvidia/deepstream/deepstream/samples/models/Primary_Detector/labels.txt
model-file=/opt/nvidia/deepstream/deepstream/samples/models/Primary_Detector/resnet10.caffemodel
deploy-file=/opt/nvidia/deepstream/deepstream/samples/models/Primary_Detector/resnet10.prototxt
batch-size=1
network-mode=2                # 0:FP32, 1:INT8, 2:FP16
num-detected-classes=4
interval=0
gie-unique-id=1
maintain-aspect-ratio=1
process-mode=1                # Primary GIE
symmetric-padding=1
model-engine-file=./models/resnet10_b1_fp16.engine

# Bbox parser for resnet10 detector
parse-bbox-func-name=NvDsInferParseCustomResnet
custom-lib-path=/opt/nvidia/deepstream/deepstream/sources/libs/nvdsinfer_customparser/libnvds_infercustomparser.so

# Preprocess
scaling-filter=0
scaling-compute-hw=0

[class-attrs-all]
pre-cluster-threshold=0.2
nms-iou-threshold=0.5

Now create the Python application main.py:

# File: main.py
import sys
import os
import time
import math
import threading
import numpy as np

import gi
gi.require_version("Gst", "1.0")
gi.require_version("GObject", "2.0")
from gi.repository import Gst, GObject

# DeepStream Python bindings
import pyds

# I2C PCA9685 (Adafruit)
import board
import busio
from adafruit_pca9685 import PCA9685

# ---------------------------
# Servo/PCA9685 control
# ---------------------------
class Diverter:
    def __init__(self, channel=0, freq=50, address=0x40):
        i2c = busio.I2C(board.SCL, board.SDA)
        self.pca = PCA9685(i2c, address=address)
        self.pca.frequency = freq
        self.ch = self.pca.channels[channel]
        # Typical servo pulses (us)
        self.min_us = 1000
        self.max_us = 2000
        self.period_us = int(1e6 / freq)
        self.lock = threading.Lock()
        # Initialize at 0 degrees
        self.goto_angle(0)

    def _us_to_duty(self, us):
        us = max(min(us, self.max_us), self.min_us)
        duty = int((us / self.period_us) * 0xFFFF)
        return max(0, min(0xFFFF, duty))

    def goto_angle(self, angle_deg):
        # Map 0..180 -> min..max us
        angle_deg = max(0, min(180, angle_deg))
        us = self.min_us + (self.max_us - self.min_us) * (angle_deg / 180.0)
        duty = self._us_to_duty(us)
        with self.lock:
            self.ch.duty_cycle = duty

    def pulse_defect(self, reject_angle=90, dwell_s=0.35, home_angle=0):
        # Non-blocking trigger; returns immediately
        def _actuate():
            self.goto_angle(reject_angle)
            time.sleep(dwell_s)
            self.goto_angle(home_angle)
        t = threading.Thread(target=_actuate, daemon=True)
        t.start()

# ---------------------------
# Global metrics and settings
# ---------------------------
class Metrics:
    def __init__(self):
        self.frame_count = 0
        self.last_ts = time.time()
        self.fps = 0.0
        self.defect_count = 0
        self.ok_count = 0

metrics = Metrics()

# Red color threshold rule in HSV (simple heuristic):
# Defect is inferred if red pixel fraction in ROI > 0.12 (12%).
RED_FRACTION_THRESH = 0.12

# Instantiate diverter on PCA9685 channel 0
diverter = Diverter(channel=0, freq=50, address=0x40)

# ---------------------------
# DeepStream pad probe
# ---------------------------
def analyze_defect(frame_rgba, obj_rect):
    """
    frame_rgba: numpy array HxWx4 (uint8)
    obj_rect: (left, top, width, height) in pixels
    Return: (is_defect: bool, red_fraction: float)
    """
    l, t, w, h = obj_rect
    H, W, _ = frame_rgba.shape
    # Clamp within frame
    l = max(0, min(W - 1, int(l)))
    t = max(0, min(H - 1, int(t)))
    w = max(1, min(W - l, int(w)))
    h = max(1, min(H - t, int(h)))
    # Take a horizontal band at the top 25% of the box to catch a red sticker
    band_h = max(2, int(h * 0.25))
    roi = frame_rgba[t:t+band_h, l:l+w, :3]  # RGB
    if roi.size == 0:
        return (False, 0.0)
    # Convert to HSV (rough approximation using numpy; avoids cv2 dependency)
    rgb = roi.astype(np.float32) / 255.0
    maxc = rgb.max(axis=2)
    minc = rgb.min(axis=2)
    v = maxc
    s = np.where(v == 0, 0, (maxc - minc) / (v + 1e-6))
    # Compute approximate hue for red: near 0 or near 1
    rc = (maxc - rgb[..., 0]) / (maxc - minc + 1e-6)
    gc = (maxc - rgb[..., 1]) / (maxc - minc + 1e-6)
    bc = (maxc - rgb[..., 2]) / (maxc - minc + 1e-6)
    h = np.zeros_like(maxc)
    # R is max
    rmask = (rgb[..., 0] >= rgb[..., 1]) & (rgb[..., 0] >= rgb[..., 2])
    h[rmask] = (bc - gc)[rmask] / 6.0
    # G is max
    gmask = (rgb[..., 1] > rgb[..., 0]) & (rgb[..., 1] >= rgb[..., 2])
    h[gmask] = (2.0 + rc - bc)[gmask] / 6.0
    # B is max
    bmask = (rgb[..., 2] > rgb[..., 0]) & (rgb[..., 2] > rgb[..., 1])
    h[bmask] = (4.0 + gc - rc)[bmask] / 6.0
    h = (h + 1.0) % 1.0  # wrap to [0,1)

    # Red band: hue in [0,0.05] U [0.95,1.0], saturation > 0.45, value > 0.3
    red_mask = ((h <= 0.05) | (h >= 0.95)) & (s > 0.45) & (v > 0.3)
    red_fraction = float(np.count_nonzero(red_mask)) / float(red_mask.size)
    return (red_fraction > RED_FRACTION_THRESH, red_fraction)

def osd_sink_pad_buffer_probe(pad, info, u_data):
    global metrics
    gst_buffer = info.get_buffer()
    if not gst_buffer:
        return Gst.PadProbeReturn.OK

    batch_meta = pyds.gst_buffer_get_nvds_batch_meta(hash(gst_buffer))
    l_frame = batch_meta.frame_meta_list
    # Map surface to CPU for pixel access
    # There is one frame in batch (batch-size=1).
    while l_frame is not None:
        try:
            frame_meta = pyds.NvDsFrameMeta.cast(l_frame.data)
        except StopIteration:
            break

        # FPS accounting
        metrics.frame_count += 1
        now = time.time()
        if now - metrics.last_ts >= 1.0:
            metrics.fps = metrics.frame_count / (now - metrics.last_ts)
            print(f"[METRICS] FPS={metrics.fps:.1f}, OK={metrics.ok_count}, DEFECT={metrics.defect_count}")
            metrics.frame_count = 0
            metrics.last_ts = now

        # Access frame pixels
        n_frame = pyds.get_nvds_buf_surface(hash(gst_buffer), frame_meta.batch_id)
        frame_rgba = np.array(n_frame, copy=True, order='C')  # RGBA

        l_obj = frame_meta.obj_meta_list
        while l_obj is not None:
            try:
                obj_meta = pyds.NvDsObjectMeta.cast(l_obj.data)
            except StopIteration:
                break

            # Use detector output class ids; but our rule is independent of class.
            rect_params = obj_meta.rect_params
            obj_rect = (rect_params.left, rect_params.top, rect_params.width, rect_params.height)
            is_defect, red_frac = analyze_defect(frame_rgba, obj_rect)

            # Update OSD text
            txt_params = obj_meta.text_params
            if is_defect:
                txt = f"DEFECT ({red_frac*100:.1f}% red)"
                metrics.defect_count += 1
                # Trigger servo
                diverter.pulse_defect(reject_angle=90, dwell_s=0.35, home_angle=0)
                # Red box and label
                rect_params.border_color.set(1.0, 0.0, 0.0, 1.0)
                rect_params.border_width = 5
                txt_params.display_text = txt
                txt_params.text_bg_clr.set(1.0, 0.0, 0.0, 0.5)
                txt_params.font_params.font_color.set(1.0, 1.0, 1.0, 1.0)
            else:
                txt = f"OK"
                metrics.ok_count += 1
                rect_params.border_color.set(0.0, 1.0, 0.0, 1.0)
                rect_params.border_width = 2
                txt_params.display_text = txt
                txt_params.text_bg_clr.set(0.0, 0.6, 0.0, 0.4)
                txt_params.font_params.font_color.set(1.0, 1.0, 1.0, 1.0)

            try:
                l_obj = l_obj.next
            except StopIteration:
                break

        try:
            l_frame = l_frame.next
        except StopIteration:
            break

    return Gst.PadProbeReturn.OK

# ---------------------------
# Pipeline creation
# ---------------------------
def create_pipeline(pgie_config):
    pipeline = Gst.Pipeline()

    # Elements
    source = Gst.ElementFactory.make("nvarguscamerasrc", "camera-source")
    if not source:
        raise RuntimeError("Failed to create nvarguscamerasrc")

    caps_src = Gst.ElementFactory.make("capsfilter", "src-caps")
    caps_src.set_property(
        "caps",
        Gst.Caps.from_string(
            "video/x-raw(memory:NVMM), width=1920, height=1080, format=NV12, framerate=30/1"
        ),
    )

    nvvidconv = Gst.ElementFactory.make("nvvideoconvert", "nvvideo-converter")
    if not nvvidconv:
        raise RuntimeError("Failed to create nvvideoconvert")

    streammux = Gst.ElementFactory.make("nvstreammux", "stream-muxer")
    streammux.set_property("batch-size", 1)
    streammux.set_property("width", 1920)
    streammux.set_property("height", 1080)
    streammux.set_property("batched-push-timeout", 33000)

    pgie = Gst.ElementFactory.make("nvinfer", "primary-infer")
    pgie.set_property("config-file-path", pgie_config)

    nvvidconv_post = Gst.ElementFactory.make("nvvideoconvert", "nvvideoconvert-post")
    caps_post = Gst.ElementFactory.make("capsfilter", "caps-post")
    caps_post.set_property(
        "caps",
        Gst.Caps.from_string("video/x-raw(memory:NVMM), format=RGBA")
    )

    nvosd = Gst.ElementFactory.make("nvdsosd", "onscreendisplay")

    sink = Gst.ElementFactory.make("fakesink", "fake-sink")
    sink.set_property("sync", False)

    for elem in [source, caps_src, nvvidconv, streammux, pgie, nvvidconv_post, caps_post, nvosd, sink]:
        pipeline.add(elem)

    # Link camera path into streammux sink_0
    source.link(caps_src)
    caps_src.link(nvvidconv)

    sinkpad = streammux.get_request_pad("sink_0")
    srcpad = nvvidconv.get_static_pad("src")
    srcpad.link(sinkpad)

    # Downstream: streammux -> nvinfer -> nvvideoconvert -> RGBA -> nvosd -> sink
    if not streammux.link(pgie):
        raise RuntimeError("Failed to link streammux to nvinfer")
    if not pgie.link(nvvidconv_post):
        raise RuntimeError("Failed to link nvinfer to nvvideoconvert-post")
    if not nvvidconv_post.link(caps_post):
        raise RuntimeError("Failed to link post-convert to caps")
    if not caps_post.link(nvosd):
        raise RuntimeError("Failed to link caps-post to nvosd")
    if not nvosd.link(sink):
        raise RuntimeError("Failed to link nvosd to sink")

    # Attach pad probe to pgie src (after inference)
    pgie_src_pad = pgie.get_static_pad("src")
    if not pgie_src_pad:
        raise RuntimeError("Unable to get src pad of nvinfer")
    pgie_src_pad.add_probe(Gst.PadProbeType.BUFFER, osd_sink_pad_buffer_probe, 0)

    return pipeline

def main():
    if len(sys.argv) != 2:
        print("Usage: python3 main.py pgie_conveyor.txt")
        sys.exit(1)
    pgie_config = sys.argv[1]
    if not os.path.exists(pgie_config):
        print(f"Config not found: {pgie_config}")
        sys.exit(1)

    GObject.threads_init()
    Gst.init(None)

    pipeline = create_pipeline(pgie_config)

    # Bus to catch errors
    bus = pipeline.get_bus()
    bus.add_signal_watch()

    def on_message(bus, message):
        t = message.type
        if t == Gst.MessageType.EOS:
            print("End-of-stream")
            pipeline.set_state(Gst.State.NULL)
        elif t == Gst.MessageType.ERROR:
            err, dbg = message.parse_error()
            print(f"ERROR: {err}, debug: {dbg}")
            pipeline.set_state(Gst.State.NULL)
    bus.connect("message", on_message)

    # Start
    pipeline.set_state(Gst.State.PLAYING)
    print("Pipeline started. Press Ctrl+C to stop.")
    try:
        loop = GObject.MainLoop()
        loop.run()
    except KeyboardInterrupt:
        print("Stopping...")
    finally:
        pipeline.set_state(Gst.State.NULL)

if __name__ == "__main__":
    main()

Build/Flash/Run commands

1) Install DeepStream (Jetson), dependencies, and Python bindings:

# DeepStream (for JetPack 5.1.x). If already installed via SDK Manager, skip.
sudo apt update
sudo apt install -y deepstream-6.3

# GStreamer runtime and Python GI
sudo apt install -y python3-gi python3-gi-cairo gir1.2-gstreamer-1.0 gir1.2-gst-plugins-base-1.0 gstreamer1.0-tools

# DeepStream Python bindings
python3 -m pip install --upgrade pip
python3 -m pip install /opt/nvidia/deepstream/deepstream/lib/python/bindings/py3/pyds-*.whl

# PCA9685 libraries
python3 -m pip install Adafruit-Blinka adafruit-circuitpython-pca9685 numpy

# I2C tools
sudo apt install -y i2c-tools
sudo usermod -aG i2c $USER
# Log out/in or reboot to apply i2c group membership

2) Performance and power mode (optional but recommended for stable metrics). Warning: MAXN and jetson_clocks increase thermals and power; ensure proper cooling.

# Query power mode
sudo nvpmodel -q

# Set MAXN (mode 0 typically)
sudo nvpmodel -m 0

# Lock clocks for consistent FPS
sudo jetson_clocks

3) Prepare working directory and configs:

mkdir -p ~/deepstream-conveyor/models
cd ~/deepstream-conveyor
# Save pgie_conveyor.txt and main.py as shown above

4) Verify camera standalone:

gst-launch-1.0 nvarguscamerasrc ! \
'video/x-raw(memory:NVMM),width=1920,height=1080,framerate=30/1,format=NV12' ! \
nvvideoconvert ! 'video/x-raw,format=I420' ! fakesink -v

5) Run the DeepStream conveyor app:

cd ~/deepstream-conveyor
python3 main.py pgie_conveyor.txt

Expected startup logs:
– nvinfer will build FP16 TensorRT engine on first run and save to ./models/resnet10_b1_fp16.engine.
– Periodic [METRICS] FPS and OK/DEFECT counts printed to stdout.

In a separate terminal, monitor system utilization:

sudo tegrastats
# Expect GPU 15–40%, EMC 10–30%, RAM usage within budget; sample output includes GR3D, EMC, CPU, RAM, FPS logs from app.

To revert power settings after testing:

# Restore default nvpmodel (query available modes with -q)
sudo nvpmodel -m 2  # example: balanced mode (value may differ)
sudo systemctl restart nvfancontrol || true

Step-by-step Validation

1) Camera framing and conveyor scene:
– Position the IMX477 above the conveyor with a fixed field of view so each item occupies a consistent fraction of the frame.
– For test validation, affix a red sticker (bright, saturated red) on items you want classified as “defect”.
– Ambient lighting should be stable; avoid flicker and shadows.

2) Pipeline brings frames to DeepStream:
– On first run, verify the engine building message from nvinfer; subsequent runs should load the cached engine instantly.
– Confirm that no ERROR messages appear. If headless, seeing only console prints is fine.

3) Observe metrics:
– In the main.py console, you should see lines like:
[METRICS] FPS=28.9, OK=112, DEFECT=8
– This indicates end-to-end flow from camera to inference to rule evaluation.

4) Servo actuation check:
– When a defect (red-labeled item) enters the field, the console should print a higher DEFECT count in the next second’s metrics update.
– The diverter servo should swing to ~90° for ~0.35 s and return to 0°.
– If you have a bin positioned, the object should be pushed or guided into the reject path.

5) Quantitative metrics collection:
– Record three 60-second runs with typical line throughput. For each run:
– Average FPS from console entries (sum/num_entries).
– tegrastats snapshot (every ~5s) to list GPU (GR3D), CPU, EMC averages.
– Count false positives: items without red sticker but actuated; and false negatives: red-sticker items not actuated.
– Compute defect trigger latency: place a red sticker crossing a marked line and measure time until servo begins moving (use a smartphone 240 FPS slow-mo if available). Target ≤120 ms.

6) Sanity of defect rule:
– Temporarily remove all red stickers; verify DEFECT count stops increasing and only OK increments.
– Show a large red card in ROI: DEFECT should spike, demonstrating color-threshold sensitivity.
– Adjust RED_FRACTION_THRESH in main.py if needed based on your lighting and sticker size (typical range 0.08–0.18).

7) Stability test:
– Continuous operation for 30 minutes with tegrastats running.
– Ensure no thermal throttling warnings; if FPS decays, improve cooling or reduce resolution to 1280×720 in pgie pipeline caps.

Troubleshooting

  • Camera not detected by nvarguscamerasrc:
  • Symptom: “No cameras available” or Argus errors.
  • Check physical CSI cable orientation and seating.
  • Ensure IMX477 driver/overlay is installed for JetPack 5.x; consult Arducam’s Jetson IMX477 guide.
  • Restart Argus daemon: sudo systemctl restart nvargus-daemon
  • Test a lower resolution: 1280×720, and verify with v4l2-ctl –list-formats-ext if using a V4L path.

  • nvinfer engine build errors:

  • Ensure DeepStream 6.3 is installed and the model paths in pgie_conveyor.txt exist.
  • Verify TensorRT libraries: dpkg -l | grep tensorrt
  • Disk permissions in ~/deepstream-conveyor/models; the process must write engine files.

  • Python pyds import fails:

  • Ensure you installed the correct wheel: pip3 install /opt/nvidia/deepstream/deepstream/lib/python/bindings/py3/pyds-*.whl
  • PYTHONPATH not required when using the wheel; if building from source, set it accordingly.

  • I2C / PCA9685 not found:

  • i2cdetect -y -r 1 should show 0x40. If not, check:
    • VCC (3.3V) to PCA9685 logic, external 5V to V+, common ground with Jetson.
    • SDA/SCL swapped or cold solder joints.
    • Address jumpers (A0–A5) if changed; update Diverter(address=0x4X).
  • Permissions: make sure you re-logged after adding user to i2c group.

  • Servo jitters or reboots Jetson:

  • Do not power servo from Jetson 5V. Use a separate 5V PSU with adequate current (≥2A).
  • Add bulk capacitor across V+ and GND near PCA9685.
  • Keep grounds common; route servo cables away from CSI ribbon.

  • Low FPS or high latency:

  • Lock performance: sudo nvpmodel -m 0; sudo jetson_clocks
  • Reduce resolution to 1280×720 in capsfilter and streammux width/height.
  • Ensure batch-size=1 and network-mode=2 (FP16) in pgie_conveyor.txt.
  • Avoid unnecessary CPU copies; we only map the frame once per batch and sample a small ROI.

  • Lighting/color false detections:

  • Increase RED_FRACTION_THRESH, e.g., to 0.18.
  • Use matte red stickers and avoid glossy surfaces; reduce reflections.
  • Add a fixed shroud and constant LED illumination above the ROI.

Improvements

  • Replace color-based rule with a proper secondary classifier:
  • Train a small MobileNetV2 classifier (OK vs DEFECT) on cropped object ROIs and add it as SGIE in DeepStream (nvinfer secondary).
  • This keeps processing GPU-accelerated and reduces false positives under variable lighting.

  • Integrate nvdsanalytics for ROI and line-cross counting:

  • Use it to trigger events only when objects cross a “decision line,” reducing double-counts.

  • Use nvv4l2h264enc and RTSP streaming:

  • Add an RTSP sink to monitor results remotely; useful for logging and QA.

  • Closed-loop conveyor control:

  • Replace servo with a 24V solenoid driven via a MOSFET and optocoupler; still command via PCA9685 (PWM 0/100% duty) or a dedicated GPIO.

  • INT8 optimization:

  • Calibrate resnet10 or custom detector for INT8 with a representative dataset to increase FPS and lower power.

  • Data logging:

  • Publish per-item decision and red_fraction to MQTT (nvmsgbroker) for analytics dashboards.

Checklist

  • [ ] JetPack/DeepStream verified; GPU and TensorRT detected.
  • [ ] Camera IMX477 produces frames with nvarguscamerasrc; stable exposure and framing.
  • [ ] PCA9685 seen on I2C bus (0x40) and servo homes to 0° on startup.
  • [ ] DeepStream pipeline runs at ≥25 FPS 1080p with FP16 and prints METRICS.
  • [ ] Defect rule triggers servo reliably for red-tagged items; minimal false positives.
  • [ ] tegrastats indicates acceptable GPU/CPU/EMC utilization; no thermal throttling.
  • [ ] nvpmodel/jetson_clocks reverted after tests if desired.

Notes on DeepStream path and performance

  • This case uses DeepStream (Option B) with a minimal nvinfer pipeline. The resnet10 Primary Detector is a light Caffe model suitable for real-time on Orin Nano. We operate it in FP16 and batch-size=1 to minimize latency.
  • Camera-only GStreamer quick test:
    bash
    gst-launch-1.0 nvarguscamerasrc ! nvvideoconvert ! 'video/x-raw,format=I420' ! fakesink
  • Expect DeepStream console output plus tegrastats metrics like:
  • [METRICS] FPS=29.6, OK=185, DEFECT=15
  • tegrastats: GR3D_FREQ 35%@585; EMC_FREQ 22%@2040; CPU@25–35%; RAM 1.4/8GB

By following the above steps, you will have a deterministic, reproducible DeepStream-based conveyor defect detection system on the exact hardware set: NVIDIA Jetson Orin Nano Developer Kit + Arducam IMX477 + Adafruit PCA9685, complete with I/O actuation and quantitative validation.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary purpose of the conveyor defect detection system?




Question 2: Which camera is used in the defect detection system?




Question 3: What is the expected frame rate at 1920×1080 resolution on the Orin Nano?




Question 4: What type of driver is used to actuate the diverter servo?




Question 5: What is the maximum end-to-end decision latency allowed?




Question 6: What is the target GPU utilization while the pipeline runs?




Question 7: In food processing, what does the system identify as potential contaminants?




Question 8: What percentage of correct actuation is expected for tagged items?




Question 9: Which component's placements does the system monitor in electronics assembly?




Question 10: What is the maximum allowed CPU utilization during operation?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top