A contour script copied from a training tutorial stops on its first findContours call with ValueError: not enough values to unpack (expected 3, got 2). The image, the mask and the arguments are all fine. The tutorial targets an OpenCV version whose Python binding returned three values; your installed release returns two. This walk-through is for engineers porting legacy OpenCV contour code onto current cameras and controllers. You will read the two counts in the error, map them to your installed version, fix the line with one edit, and write the call so it survives the next version change.
What the error message tells you
Python unpacks a tuple element by element, so the number of names on the left of = must equal the number of elements the call returns. The message states both counts. "Expected 3" is your script: three names, _, contours and hierarchy. "Got 2" is the function: the installed cv.findContours returned a two-element tuple, (contours, hierarchy). The traceback points at the exact line, so the diagnosis costs one read, not one debugging session.
If instead the call runs and you get a strange number of outlines, that is a different failure with a different fix — thresholding and compression artifacts — covered in findContours returns the wrong number of contours.
Why the return tuple changed between OpenCV versions
| OpenCV series |
findContours returns |
Names to unpack |
|---|---|---|
| 2.4 | (contours, hierarchy) |
2 |
| 3.x | (image, contours, hierarchy) |
3 |
| 4.x | (contours, hierarchy) |
2 |
The 3.x binding handed back the input image first, because the function used it as its workspace while tracing outlines. OpenCV 4 stopped modifying the source image, so the extra return lost its purpose and was dropped. A tutorial written against 3.x therefore unpacks three values, and the same line on 4.x raises exactly the error in this title.
The fix: unpack two values, or slice the last two
# Fails on OpenCV 4.x: three names, two elements
_, contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
# Works on OpenCV 4.x and 2.4: two names, two elements
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
Keep both names when you draw nested outlines, because cv.drawContours takes the hierarchy as the argument after the line type, exactly as the original script passed it. If you never use the tree, assign it to _:
contours, _ = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
For code that must run on 3.x and 4.x alike, capture the whole return and slice the last two elements. Contours and hierarchy are the final two items in every series, so one line covers all of them:
result = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
contours, hierarchy = result[-2:] # contours: tuple of NumPy arrays; hierarchy: NumPy array
Worked example: the tutorial line on OpenCV 4.5.1
Inputs: the installed release is 4.5.1, a current 4.x build (any 4.x returns the same two-element form); the failing line is the tutorial's assignment with three names; the arguments are cv.RETR_TREE and cv.CHAIN_APPROX_SIMPLE, unchanged; the input is the single-channel uint8 mask produced by cv.inRange.
Rule: Python assigns tuple elements to names positionally, and the two counts must match.
Arithmetic:
- Names on the left: 3 (
_,contours,hierarchy) - Elements returned on 4.x: 2 (
contours,hierarchy) - Surplus names: 3 − 2 = 1 →
ValueError: not enough values to unpack (expected 3, got 2) - After the fix: 2 names vs 2 elements → difference 0 → the assignment completes and
contoursholds the outlines
Decision: the digit before the first dot in the version string is the switch. Major version 3 → unpack three names. Major version 2 or 4 → unpack two names. The slice form makes that decision for you at every call site.
Tests that prove the fix took
Run these three prints once, in order:
print(cv.__version__) # '4.5.1' — confirms which signature your environment has
print(len(result)) # 2 — confirms the two-element return
print(len(contours)) # 1 — one bright region in the mask gives one contour
Then draw, passing the hierarchy as the original script intended:
cv.drawContours(img, contours, -1, (255, 0, 0), 3, cv.LINE_AA, hierarchy)
cv.imshow('contours', img)
cv.waitKey()
cv.destroyAllWindows()
One detail worth knowing while you look at the window: the colour tuple is BGR in OpenCV, so (255, 0, 0) draws blue outlines, not red. If the window shows outlines and the process exits without a traceback, the port is done.
The same line in a live capture loop
In production the frame arrives from a camera rather than a JPEG file, but the chain is unchanged: each frame lands in your process as a NumPy array, cv.cvtColor and cv.inRange build the mask, and the fixed findContours call runs per frame. Fix the unpack once, at the call site, and every frame after it behaves.
Streaming has its own failure modes, covered separately: keep three frames in the capture buffer, not one, and if reads start returning false, warm the capture up as in OpenCV cap read returns false on Raspberry Pi.
For a bench station that processes these frames, start from the area scan cameras category or the camera finder, and size the camera before the lens with the camera configurator:
- Sensor format
- 1/1.7"
- Interface
- USB3
- Resolution
- 12 MP
- Shutter
- Rolling
The LS-CMR-0166 streams 4000 × 3000 frames at 30 fps over USB3, so the threshold-and-contour chain runs on every frame at full rate. To run the loop unattended next to the camera, look at the vision controllers category:
- LAN ports
- 3-4 LAN ports
- USB 3 ports
- 2 USB 3 ports
- Light outputs
- 4 built-in channels
- Memory
- 8 GB
The MV-VC2140-128G40-1T supplies 2 USB 3 ports for the camera and 4 built-in light channels for the illumination, with 8 GB of memory for the capture buffers and the contour data.
When the call still fails after the fix
Two environments, two signatures: the same script passes on your desk and fails on the cell PC because one box still runs a 3.x build. Pin the OpenCV version per project the way you pin any dependency — the OpenCV version conflicts that crash vision calibration tools case is the same disease with a different symptom.
If the traceback names a different line, check that nothing earlier overwrote cv or reassigned findContours; the error follows the object, not the file.
And a quiet result is not an error: an empty mask returns zero contours, len(contours) prints 0, and drawContours with index -1 simply draws nothing.
Habits that keep contour scripts portable
- Print the version string at start-up and log it beside your results. One glance settles signature questions months later.
- Standardise on one call pattern — the slice form — so a version bump never revisits this error.
- Feed the function a clean mask: noise in the mask inflates the contour count and every downstream cost, which is where the blob analysis finds too many blobs case starts.
- On 4.x, call
findContourson the mask directly. The function no longer modifies it, so the defensive.copy()in old scripts is dead weight. - When you copy a tutorial, note the OpenCV version it was written against before trusting any signature from it.
From here the contours are yours to work with: coordinates, areas, intersections — the contour and line intersection coordinates walk-through picks up exactly where this fix ends. If the set-up around the code needs eyes, the contact page reaches the applications team, and more fixes of this kind sit on the systems topic page.
Questions
What is inside the hierarchy array returned by findContours?
One row per contour, holding four indices into the contour list: next contour at the same level, previous contour at the same level, first child, and parent. An index of -1 means that relative does not exist. drawContours uses the array to walk the tree, which is why the original script passed it after the line type. If you only need outer outlines, you can ignore it entirely.
Does the unpacking change affect C++ code as well?
No. The C++ function fills vectors you pass in by reference, so there is nothing to unpack and the call keeps its shape across versions. The version change in C++ is about the source image: 3.x used it as a workspace and could modify it, 4.x leaves it untouched. Only the Python binding changed how many values the call hands back.
Why do I now get 'too many values to unpack (expected 2)'?
The mirror case: your installed OpenCV is 3.x, which returns three values, and the script unpacks two. Keep the underscore placeholder first — image, contours, hierarchy — or switch to the slice form, which fits both signatures. The counts in the message still tell the story: expected 2 is your script, got 3 is your version.
Can findContours run on a colour image directly?
Give it an 8-bit single-channel mask. Non-zero pixels count as foreground and zero pixels as background, so run cv.inRange or a threshold first and pass the result. The failing tutorial actually did this part correctly: it converted BGR to HSV with cv.cvtColor, built the mask with cv.inRange, and only then called findContours.
Is thresh.copy() still needed before findContours on OpenCV 4?
No. OpenCV 4 leaves the mask untouched, so the defensive copy in legacy scripts only burns memory on every frame. On 3.x the copy was protective, because the function used the input image as its workspace. Drop the copy after confirming your installed version with the version string print.

