System designHow-to

Read a sequence of images and log pixel values to file

Read a sequence of images without error -1074395995: list the folder, verify each path before the read call, then log per-frame pixel values to CSV.

LumaSpec Vision10 min read Share on LinkedIn

On this page 9 sections
  1. 01What error -1074395995 actually is
  2. 02What you need
  3. 03The loop: list, verify, read, convert, log
  4. 04Worked example: 500 frames from a 12 MP camera
  5. 05Log statistics, not every pixel
  6. 06Why only the first frame loads, and other classics
  7. 07Verify the run
  8. 08Checklist before the next run
  9. 09Questions

You have a folder of frames on disk and a program that should read a sequence of images, pull the pixel values out of each one, and write them to a file. Instead the run stops at the read call with error -1074395995: image file not found. In almost every case the camera, the cable and the driver are innocent — the string handed to the read call does not point at a file that exists. This guide walks the batch loop end to end: building the file list from the folder itself, verifying each path before the read, converting each frame to a pixel array, and logging one statistics row per image to a CSV, with the arithmetic for how big each output gets and the checks that prove the run.

What error -1074395995 actually is

Error -1074395995 is the read call's file-not-found code: the path string it was handed does not resolve to a file on disk. Nothing in that sentence involves acquisition hardware. The frames already exist; the loop never reached them. Five causes cover nearly every case:

  • The file list is generated by naming (frame_001.png, frame_002.png, and so on) instead of taken from a directory listing, and the numbering has a gap or starts at a different number.
  • The loop index runs one entry past the end of the list, so the last generated name points at nothing.
  • The extension or letter case does not match the files (.png versus .PNG, or a folder full of .bmp).
  • A relative path such as ..\frames\ resolves against the program's current working directory, which changes with how the program is launched.
  • The path input is empty on every iteration after the first.

The decision follows directly: before you touch a driver, capture the exact string at the moment of failure and compare it, character for character, with a directory listing. The mismatch is in that comparison, and once you see it the fix is one line.

What you need

Three things: a folder of frames with consistent names, the program that runs the loop, and somewhere to write the CSV. Zero-padded numbering (frame_0007.png) is worth adopting now, because it sorts into frame order as plain text with no custom code.

The frames come off area scan cameras at capture time. The worked example below uses a 12 MP USB3 model, the LS-CMR-0166, which writes 4000 × 3000 frames at 30 fps.

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

If you capture the sequence first and read it back afterwards, the capture side lives in frame grabbers — a 4-port GigE model such as the MV-GE1104 — and the batch loop runs on a vision controller, for example the MV-VC3301H-128G66, an Intel Core i3 machine with 8 GB of memory. Neither is in the path between your loop and a file on disk, so neither can raise the file-not-found error.

Frame grabbersHikrobot MV-GE1104 4-port GigE frame grabberMV-GE1104
Interface
GigE
Ports
4 ports
$699.00View part
Vision controllersHikrobot MV-VC3301H-128G66 I3-8100, 8GB+128G SSD, No PoE, PCIe expansion supported, Incl. VM6200Pro licenseMV-VC3301H-128G66
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
$2,419.00View part

The loop: list, verify, read, convert, log

  1. List the directory once, before the loop starts. Filter by extension and store full paths. The listing, not a name generator, is the loop's input; this one change removes the gap-and-crash failure mode entirely.
  2. Sort into frame order. Zero-padded names sort correctly as text. Unpadded names need a natural sort that extracts the number from each name, because plain text order puts frame_10.png before frame_2.png.
  3. Check that each path exists, then read that same string. One exists-check, one read, one variable. Decide the missing-file policy once, at the top: log the gap and continue, or stop the run.
  4. Check the image after the read. An empty image or the wrong width and height is a five-second fix here; the same frame reaching your write stage looks like an unrelated bug — the failure in an empty frame hitting the save call.
  5. Convert the image to a 2D pixel array. This is the decode step where stored file data becomes pixels you can index; handling raw file bytes directly is a silent-error classic.
  6. Compute the statistics in one pass. Minimum, maximum, mean and standard deviation need one traversal of the array. Reuse one buffer for the array instead of allocating per frame — the pattern in reusing image buffers instead of allocating per frame.
  7. Append one CSV row per frame — name or number, then the statistics. Open the file once before the loop, write rows inside it, close at the end; flush after every row instead if a crash must not lose what is already computed.
  8. Count rows against files. Rows written must equal files found, plus the gaps you chose to log. Anything else means the run is incomplete.

The loop in outline:

file_list = list_directory("D:/frames", filter="*.png")  # list of strings, natural sort
csv_file  = open("D:/frames/stats.csv", "write")         # opened once, closed at the end
csv_file.write("frame,min,max,mean,std")
for path in file_list:
    if not file_exists(path):
        csv_file.write(file_name(path) + ",,,,missing")  # policy: log and continue
        continue
    image = read_image(path)                             # returns -1074395995 when the path fails
    if image_width(image) != 4000 or image_height(image) != 3000:
        csv_file.write(file_name(path) + ",,,,size")
        continue
    pixels = image_to_array(image)                       # 2D array of integers, 0-255
    csv_file.write(file_name(path) + "," + str(array_min(pixels)) + "," + str(array_max(pixels))
                   + "," + str(array_mean(pixels)) + "," + str(array_std(pixels)))
csv_file.close()
assert count_rows("D:/frames/stats.csv") == count(file_list)  # the test that proves the run

Worked example: 500 frames from a 12 MP camera

Inputs: 500 PNG frames, each 4000 × 3000 pixels, 8-bit mono with values 0-255, captured at 30 fps, read back offline on the controller. The frame size is the camera's printed resolution; the 8-bit depth and the 500-frame count are this example's inputs.

One frame holds 4000 × 3000 = 12,000,000 pixels, so 12,000,000 B, about 11.4 MiB uncompressed. The folder holds 500 × 12,000,000 B = 6.0 GB of raw pixel data; PNG files compress that, so size the disk on the raw number.

In one frame the pixel sum came to 1,424,400,000, so the mean is 1,424,400,000 ÷ 12,000,000 = 118.7, alongside min 12, max 243 and standard deviation 41.3 — values from one example folder, and exactly the arithmetic you apply to every frame.

The CSV row for that frame reads 000217,12,243,118.70,41.31: 26 characters, about 30 B with the line ending. The whole run writes 500 × 30 B ≈ 15 kB, a file any spreadsheet opens.

Writing every pixel value instead costs 12,000,000 values × 4 B (three digits plus a comma) = 48,000,000 B, about 46 MiB per frame, and 500 × 48 MB = 24 GB for the folder. The statistics row is smaller by a factor of 48,000,000 ÷ 30 = 1,600,000.

Decision: statistics for every frame, full pixel dumps only for the frames those statistics flag. Keep the batch offline — the 30 fps capture rate sets a 1 ÷ 30 s = 33.3 ms per-frame budget that no disk-reading statistics loop wants inside it. If processing must keep pace with capture, buffer frames and process behind the loop, the pattern in saving an image sequence from a frame grabber with a ring buffer.

Log statistics, not every pixel

mean = sum(p) / (W * H) std = sqrt(sum((p - mean)^2) / (W * H)) text_bytes = W * H * (digits_per_value + 1)
What you write Per 12 MP frame 500 frames Choose it when
Whole-frame row: name, min, max, mean, std ~30 B ~15 kB Every run: exposure drift, first-pass screening
Two ROI rows: part region and background ~60 B ~30 kB Separating part changes from lighting changes
Every pixel value as text ~46 MiB ~24 GB Only the frames the statistics flag

A region of interest is a sub-array — rows a to b, columns c to d — and its statistics use the same formulas with the region's pixel count in the denominator. To map a sub-array back to coordinates in the original image, locating image coordinates from a 2D pixel array walks through the index arithmetic.

Watch the bit depth. An 8-bit frame holds 0-255; a 12-bit frame holds 0-4095. Log the values the camera produced: a 12-bit mean of 1187 rescaled to 8 bits becomes 1187 × 255 ÷ 4095 ≈ 74, indistinguishable in the log from a genuine 8-bit 74. Frames that arrive as 12-bit files are covered in saving 12-bit camera images without losing intensity data.

And know what a pixel value is: a digital number set by exposure, gain and lighting, not a measurement in lux — can you convert pixel values to lux with a camera covers why. When exposure or gain changes between folders, the means stop being comparable; log both as extra CSV columns so every trend can be read against the settings that produced it.

Why only the first frame loads, and other classics

When the loop reads exactly one image and stops, the read call sits outside the loop or the list index never advances — the wiring behind image sequence import: why only the first frame loads. The file-not-found variant has its own classics:

  • Index off by one: names start at 001 while the index starts at 0, so the run fails at the last file or one past it.
  • An extension filter that matches nothing: the listing step returns an empty list and the first generated name fails immediately.
  • Case and separators after a move between Windows and Linux: IMG_001.PNG is not img_001.png on a case-sensitive system, and a backslash is a literal character outside Windows. The image file path errors across operating systems guide lists the exact failures.
  • A default path input consumed by the first call and empty on every call after it.

The decode failure completes the set: reading a PNG file as raw bytes and treating them as pixel values hands your statistics a header. Decode to an array first; image bytes versus pixel data: decode before processing shows the failure.

Verify the run

  1. Row count equals file count plus the gaps you logged.
  2. Every mean sits between its own min and max, and inside 0-255 for 8-bit frames (0-4095 for 12-bit). A row outside that range is a decode problem, not a brighter part.
  3. Frame numbers strictly increase with no duplicates; a duplicate means the sort or the name extraction is wrong.
  4. Recompute one frame independently: open frame 000217 in any viewer that reports a histogram and check its mean against the CSV row to rounding.
  5. Count the missing rows. Those are exactly the frames a name generator would have crashed on.

Checklist before the next run

  • File list built from a directory listing, filtered by extension — a generated list re-creates the crash the day the numbering skips.
  • Natural sort, or zero-padded names — otherwise frame 10 is processed before frame 2 and every trend lies.
  • Exists-check and read use the same string variable — two builds of one path is the verified-but-not-found trap.
  • Empty and size check after every read — catches a truncated file before it poisons the statistics.
  • One CSV row per frame, fixed columns, flush policy chosen — the row-per-frame structure is what makes the trend plottable.
  • Row count reconciled against the file count — the run is not finished until they match.

More acquisition-side debugging lives under the systems topic. When the CSV is clean and the folder is filling faster than one machine reads it, build the whole chain — camera, lens, light, controller — in the system configurator, or send the folder details to us through the contact page.

Questions

What is error -1074395995, and do I need to reinstall the driver?

No. Error -1074395995 is the read call's file-not-found code: the path string handed to it names no file on disk. The camera, cable and driver sit upstream of a folder that already exists, so reinstalling changes nothing. Log the exact path at the moment of the failure, compare it with a directory listing, and fix the list, the sort or the index that built the string.

Why does my loop fail only on the last image?

An off-by-one index. When the file names start at 001 and the index starts at 0, the loop asks for one name past the last file; when the loop bound is the file count plus one, the same thing happens. Iterate over the directory listing itself instead of a generated name, and let the last listed file be the last iteration — never a count.

Should every pixel value go into the CSV?

No — not for a whole folder. One 12 MP, 8-bit frame is about 46 MiB written as text, and a 500-frame folder about 24 GB, against roughly 30 B per statistics row. Log min, max, mean and standard deviation for every frame, and write full pixel dumps only for the few frames those numbers flag. You keep the trend, and the flagged frames keep their evidence.

How do I keep frame_2.png ahead of frame_10.png?

Zero-pad the names at capture — frame_0002.png — and plain text sorting becomes frame order. For a folder that already exists with unpadded names, sort by the number extracted from each name instead of the whole string. Renaming ten thousand files once, with any batch rename tool, costs less than maintaining a custom sort in the program forever.

The mean dropped but the line did not change. What moved?

A camera setting, not the part. Pixel values track exposure time, gain and light output; when any of them changes between folders the means stop being comparable even though the product is identical. Find the first folder where the shift appears and read the settings logged beside it. Shooting one plain white reference frame at the start of every batch gives you a canary: when the reference mean moves, the setup moved.