Skip to content

EN: Object detection in camera streams#7490

Merged
TEParsons merged 12 commits intopsychopy:devfrom
mdcutone:dev-en-opencv-cameras
Dec 10, 2025
Merged

EN: Object detection in camera streams#7490
TEParsons merged 12 commits intopsychopy:devfrom
mdcutone:dev-en-opencv-cameras

Conversation

@mdcutone
Copy link
Copy Markdown
Member

@mdcutone mdcutone commented Dec 3, 2025

This PR adds object detection capabilities to the camera interface. The user can define classifiers and pass them to an object detection method which returns data about any detected objects (position, size, etc.)

This PR also adds a new CameraFrame object which encapsulates frame data and contains the new detectObjects() method. To perform object detection, first create a new classifier:

faceDetector = imagetools.HaarCascadeObjectRecognizer('haarcascade_frontalface_default.xml', name='face')

Then pass the classifier to the detectObjects() method of desired frame to detect objects:

# using the most recent frame (i.e. lastFrame)
if camera.lastFrame is not None:
    facePos = camera.lastFrame.detectObjects(faceDetector)

Returns the results as such:

{'face': {'pts': 16.064490300006582, 'count': 1, 'objects': [{'rect': (31, -167, 151, 151), 'center': (31, 16), 'bbox': ((271, 153), (422, 153), (422, 304), (271, 304))}]}}

This also supports batch object detection which allows the user to specify a set of classifiers and pass them all at once to detectObjects as a list. This is more efficient than calling the method multiple times since it avoids multiple color conversions.

@codecov
Copy link
Copy Markdown

codecov bot commented Dec 3, 2025

Codecov Report

❌ Patch coverage is 8.33333% with 253 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.78%. Comparing base (abb674c) to head (a893924).
⚠️ Report is 14 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #7490      +/-   ##
==========================================
- Coverage   48.93%   48.78%   -0.16%     
==========================================
  Files         355      355              
  Lines       64990    65246     +256     
==========================================
+ Hits        31806    31829      +23     
- Misses      33184    33417     +233     
Components Coverage Δ
app ∅ <ø> (∅)
boilerplate ∅ <ø> (∅)
library ∅ <ø> (∅)
vm-safe library ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@TEParsons TEParsons self-requested a review December 4, 2025 14:06
Copy link
Copy Markdown
Contributor

@TEParsons TEParsons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looks good and seems to be working great! For the record this is the code I used to test it:

from psychopy import visual
from psychopy.tools import imagetools
from psychopy.hardware import camera
import time

# setup camera
for profile in camera.CameraDevice.getAvailableDevices():
    cam = camera.Camera(profile['device'])
    break
# setup detector
faces = imagetools.HaarCascadeObjectRecognizer('haarcascade_frontalface_default.xml', name='face')
# setup window
win = visual.Window(checkTiming=False)
# setup tracking dot
dot = visual.Rect(win, fillColor="red", units="norm")

# start camera
cam.start()
# start frame loop
start = time.time()
while time.time() - start < 10:
    # get camera frames
    cam.poll()
    # do detection
    if cam.lastFrame:
        found = cam.lastFrame.detectObjects(faces)
        # if we found a face...
        if found:
            if len(found['face']['objects']):
                # get size and position
                pos = found['face']['objects'][0]['center']
                size = found['face']['objects'][0]['size']
                # set dot pos (adjusting for unit space)
                dot.pos = [
                    (i * 2) / total
                    for i, total in zip(pos, cam.lastFrame.frameSize)
                ]
                # set dot size (adjusting for unit space)
                dot.size = [
                    (i * 2) / total
                    for i, total in zip(size, cam.lastFrame.frameSize)
                ]
                # draw dot
                dot.draw()
    # draw to win
    win.flip()
    # sleep so other stuff can happen
    time.sleep(0.01)
# stop camera
cam.stop()

and the dot on screen seemed to move (albeit flipped horizontally) in the same way I moved my head around in front of the camera.

@TEParsons TEParsons merged commit 84ac056 into psychopy:dev Dec 10, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants