NegPy models the film curve instead of just inverting the negative
Tool for processing film negatives.
At a glance
- What is it?
- NegPy is a GPL-3.0 Python and PyQt6 application for turning scans of film negatives and slides into finished pictures, with a density space conversion built on an H and D curve, paper profiles from manufacturer datasheets, and hardware support for film scanners and tethered cameras. It runs on Linux, macOS and Windows with GPU rendering through wgpu.
- Who is it for?
- NegPy fits a film photographer who scans negatives and wants a conversion that behaves like a darkroom print rather than an inversion, who has a supported scanner or a tethered camera setup, and who is prepared to meter each roll and check the result.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The inversion is the easy part
The README opens with the reason the project exists, and it is a good framing. NegPy turns scans of film negatives and slides into finished pictures. I wrote it because I wanted a tool made for film scans that does more than invert them. It models film and photographic paper, and adds the conveniences of a lab scanner.
That last clause is the design position. A negative scan is an inversion followed by a colour transformation, and doing the inversion by hand with a curves tool gets you something that is legible but not correct, because the film's response is not linear and neither is the paper. NegPy puts both in the model.
The conversion section describes what that means in practice. The orange mask is removed from each channel's own sensitometry, with no camera profiles and no border color picking. The print is built in density space on an H&D curve, with toe, straight section and shoulder, and graded in ISO-R points like darkroom paper. Auto Density and Auto Grade meter each frame to adjustable targets. And there are paper profiles from Ilford, Kodak, Foma and Fuji datasheets.
So the pipeline is a physical simulation, not a lookup. The orange mask handling in particular is the part that separates a real tool from a script, because the mask is per channel and per film stock, and estimating it from a border is a shortcut that only works if the border is intact. Working from the film's own sensitometry avoids that dependency.
The modes cover the three cases a scanner user faces. Color negative, B&W and slide modes, and slides open as captured, with optional normalization for faded film and HDR merging of bracketed exposures.
The documentation structure supports this being a serious piece of software. The User Guide covers every panel and control, and the info button on each panel opens it in the app, and PIPELINE.md explains the math.
Scanning hardware is a first class concern
The Capture section lists the hardware, and the list is specific enough to be useful.
Film scanners supported are the Plustek OpticFilm 8200i SE, 8100 V2, 8300i SE and 7600i v1 over USB, the Nikon Coolscan, Reflecta, Pacific Image, and anything SANE supports.
Tethered camera scanning is supported on macOS and Linux, including narrowband RGB captures with a Scanlight and a sub-pixel Trichrome merge, with a guide in docs/CAMERA_SCANNING.md. Narrowband capture is the technique where each of the three channels is photographed separately through a narrow band filter, which gives much better colour separation from a cheap sensor than a single RGB shot, and the Trichrome merge is the sub-pixel alignment work needed to combine the three exposures into one clean image.
Around that there is calibration. Flat-field correction, sensor calibration for narrowband light, and lens correction read from the file.
The formats list is long and includes the ones people actually have. Camera RAWs, TIFF, DNG, JPEG XL and scanner formats such as Pakon, Coolscan NEF, Flextight FFF and Noritsu.
Two platform limits are worth noting. Camera scanning is not available on Windows, because the gphoto2 dependency has no Windows build, which the pyproject.toml comments confirm. And on Windows the Plustek OpticFilm scanners need the WinUSB driver, installed with Zadig in place of the SilverFast driver, with a separate document for that.
The optional dependencies in pyproject.toml map the hardware support to extras, with nkscan, plustek, sane and camera as named groups, and the camera extra carries a platform marker of sys_platform != win32.
Rolls, and the analysis that normalises them
The Rolls section is the part of the feature list that shows this is an application rather than a converter, because it deals with the reality of a roll of film rather than a single frame.
The library is a list of rolls, which are imported folders or virtual rolls built from any frames. Roll-wide defaults cover film mode, crop, calibration, metering, raw decode, lens and metadata, with per-frame overrides. And Roll Analysis gives every frame of a roll, or of a scene within it, the same baseline.
That last one is the feature that matters most for a real roll. A roll of film developed as a batch has exposure variations from the metering, and a set of scans of the same scene will have a colour and density drift across the roll. Normalising every frame to the same baseline is what a lab does and what a per frame conversion cannot do, and it is the difference between a set of images that look like one session and a set that look like one session with one frame wrong.
The triage features are the practical ones. Keep and reject triage, half-frame splitting, stitching, and search by metadata or, opt-in, by content.
Half-frame splitting matters because half frame cameras produce two frames per negative, and splitting them is a per frame operation that the roll model has to support. Stitching is the opposite, joining frames. And the content search being opt-in is a good sign, since an index of every frame's visual content is a cost the user should opt into rather than get by default.
The editing side is also extensive. Dodge and burn with polygon, oval and card-edge masks, each with its own strength, feather, contrast grade and tone limit. Dust, hair and scratch removal, automatic, from the IR channel, by the NegPy or OpenICE method, or painted, keeping the film grain. And a set of darkroom tools, test strips, ring-around, step wedge, zone overlay, spot densitometer and zone placement, plus contrast mask, tilt and swing, edge burn, filed carrier and print mats. Lith, cyanotype and toners for B&W.
The GPU path and the override file that controls it
Rendering is stated as GPU rendering with Vulkan, Metal or DX12, with a CPU fallback that gives the same result. The same result is the important part of that sentence, because a fallback that produces a different image is not a fallback, it is a different result, and a photographer comparing prints would notice.
The mechanism is wgpu, at 0.31.1 in the dependency list, which is a portable graphics abstraction over Vulkan, Metal and DirectX 12. That is the right choice for an application that ships on three operating systems, since it means one rendering path in the code rather than three.
The configuration is unusually well thought out for a desktop application, and it is worth reading because it shows the author has seen the failure modes. The override.toml file is written on first run, and if NegPy crashes on startup or renders wrong you edit it. Most performance values are also in Preferences, but the file wins.
[rendering]
# Options: "auto", "vulkan" (Linux/Win), "dx12" (Win), "metal" (macOS), "cpu"
backend = "vulkan"
[display]
# Qt scene-graph backend. Options: "auto", "vulkan", "d3d12", "metal", "opengl", "software"
qt_rhi_backend = "auto"
# Window system plugin (Linux only). Options: "auto", "xcb", "wayland"
qt_platform = "auto"
[performance]
# Cap GPU texture size in pixels — useful on low-VRAM cards. "auto" = no limit.
max_texture_size = "auto"So there are two separate graphics choices, the wgpu backend and the Qt scene graph backend, which is correct because they are different layers, and a per platform option list for each, including a software option for the Qt side.
The texture size cap is a good example of a real problem being solved. A high resolution scan on a low VRAM card exceeds the maximum texture dimension the GPU supports, and the failure is an error rather than a graceful reduction. Capping it explicitly is a fix for that.
The data location section is equally practical. Everything is in Documents/NegPy, with edits.db for edits, settings.db for settings, cache/ for thumbnails which is safe to delete, export/ for the default export folder, icc/ for paper and printer profiles, and override.toml for startup overrides. On Windows, if that folder is blocked, NegPy asks for another and suggests Local AppData, and NEGPY_USER_DIR overrides both.
Non-destructive edits keyed by file content
The Your data section is short and contains the design decision that makes the application usable in a real workflow.
Non-destructive: source files are never modified. Edits live in a local SQLite database keyed by file content, so files can move. Optional .negpy sidecars sit next to the scans. Undo history and named work prints survive a restart. And keyboard shortcuts can be remapped.
The keying by file content rather than by path is the part that matters. A path-keyed edit store breaks the moment a scan is moved, which happens constantly, because a scanner writes to a temporary directory, a photo application reorganises folders, or a user tidies up. A content-keyed store means the edits follow the file, which is a small implementation decision with a large effect on whether a user keeps using the tool.
The sidecar option is the alternative for people who want their edits to travel with the scans, and a .negpy file next to a scan is the kind of thing that survives a backup of a folder. Both models are supported, which is a sensible thing to offer rather than insisting on one.
The output section carries the professional features. ICC color management, monitor profile detection and soft proofing. Export to JPEG, TIFF, PNG, WebP and JPEG XL, with borders, presets, contact sheets and filename templates. A flat 16-bit TIFF master for other editors, or a linear export that skips the pipeline. And gear and capture metadata written to EXIF and XMP.
That last one closes a loop that matters for a scanner workflow. A negative scan carries almost no metadata, so the camera, lens and film stock have to come from somewhere, and writing them into the exported file is what makes the image findable in a library later.
The dependency pins are documentation in comments
The pyproject.toml is worth reading for its comments, because they explain decisions that would otherwise look arbitrary.
Almost everything is pinned to an exact version. rawpy at 0.27.0 for RAW decoding, tifffile and imagecodecs, numpy at 2.4.4, opencv-python-headless at 4.13.0.92, pillow at 12.2.0, pyqt6 and pyqt6-charts at 6.11.0, numba at 0.65.1, jinja2 at 3.1.6, piexif for EXIF, qtawesome for icons, wgpu at 0.31.1 and pyserial for the scanner serial protocols.
And then the comment that explains one of the pins.
# Pinned below its latest release: 1.24.0+ dropped macOS x86_64 wheels entirely,
# which breaks install on an Intel Mac. 1.23.2 is the last version with one.
"onnxruntime==1.23.2",So onnxruntime is held back a major version to keep Intel Macs working, and the reason is recorded next to the pin so that whoever comes along does not helpfully upgrade it. That is the single most valuable comment in the file, and it is the kind of thing that only appears in a project that has been burned by it.
The project metadata is honest about its status. The development status classifier is 3 - Alpha, the environment is GPU, the intended audience is End Users/Desktop, and the operating system classifiers cover Windows, macOS and POSIX Linux. Python 3.13 or newer is required, and the version is dynamic, read from a VERSION file at the root.
The tooling is modern Python. The dev group has pyinstaller for the builds, pytest and pytest-cov for the tests, ruff for linting and ty for type checking, and the Makefile drives all of it through uv.
all: format lint type test
lint:
@echo "Running lint checks (ruff)..."
@$(UV) ruff check .The Makefile also contains a long comment about a subtle bug, that a NEGPY_USER_DIR value which is not absolute means make did not expand what was written, since it leaves ~ alone and reads $HOME as the variable H followed by OME, and either one quietly puts the development user directory inside the checkout. So there is a guard that stops the build with an error message telling you to write $$(HOME)/your-dir instead.
Where NegPy is the wrong tool
The first wrong case is a single scan that just needs inverting. The pipeline here is density space conversion, an H and D curve with toe, straight section and shoulder, ISO-R grading, per film stock paper profiles, roll analysis and metering. If you have one scan and want a positive, a levels or curves adjustment in any image editor does it. NegPy earns its complexity over a roll of frames, not over a single file.
The second wrong case is hardware that is not supported. The scanner list is specific, the camera scanning path needs a tethered camera, and Windows has no camera scanning at all. If your scanner is not on the list and SANE does not cover it, there is no path.
The third wrong case is a workflow that needs the file format to be final on disk. The application is built around non-destructive edits in a SQLite database, so the result lives in the application and in the database rather than in the file. A team that hands files to a third party needs the exports, and the flat 16-bit TIFF master and the linear export exist for that, but the working state does not live in the file.
The fourth wrong case is expecting a stable release. The development status is alpha, the README says the builds are not signed so expect a warning on first run, and the version numbering moves fast, with 0.62.0, 0.63.0 and 0.64.0 all tagged in the three weeks before 2026-10-11. The application updates itself from an Update Available notice, so the behaviour on a given day is whatever the latest release was that day.
How it differs from a generic raw converter
The comparison worth making is with a raw development application, which is what most people would reach for, and the difference in approach is what the model is.
A raw converter starts with the camera's sensor data and a camera profile, and the profile is the accuracy mechanism. It is a per camera and per lens table that was measured by shooting a chart, and it is a general purpose model of a modern sensor's response to light. Applying it to a film negative is the wrong model, because a negative was not exposed by a sensor and has no camera profile. What a negative has is the film's own characteristic curve.
NegPy uses the film's sensitometry instead, and removes the orange mask from each channel's own sensitometry. That is the difference in approach, and it is the reason the conversion does not need border colour picking, a shortcut that depends on the scan having an unexposed border.
The second difference is the output side. A raw converter ends at a rendered image. NegPy ends at a print, and the print is built in density space on an H and D curve and graded in ISO-R points, which is the language of darkroom printing, and there are paper profiles from manufacturer datasheets. So the application has a paper simulation as well as a film simulation, and the combination is what a lab does.
The third difference is the batch behaviour. Raw converters are per image, and every image gets the same default treatment. NegPy has a roll model, with roll-wide defaults, per frame overrides, and a Roll Analysis that gives every frame of a roll or of a scene within it the same baseline. That is the operation a scanner workflow actually needs, because a roll developed as a batch has drift across it.
The fourth difference is the tooling around the image. There are darkroom tools, a spot densitometer, test strips, step wedges and zone overlays, plus dodge and burn with masks and dust and scratch removal that keeps the film grain. A raw converter has none of this, because it is a different job.
Editorial conclusion
NegPy fits a film photographer who scans negatives and wants a conversion that behaves like a darkroom print rather than an inversion, who has a supported scanner or a tethered camera setup, and who is prepared to meter each roll and check the result. It does not fit someone who only wants to flip a scan into a positive, since the pipeline is a large amount of machinery for that, and it does not fit a Windows user who needs tethered camera scanning, which the README says is not available on Windows. Before adopting it, read PIPELINE.md for the math and the User Guide for the panels, note that the project is classed as alpha with Python 3.13 or newer required, and check the release list, where 0.64.0 was tagged on 2026-10-11 with the last push on 2026-10-15.
Frequently asked questions
What does NegPy do differently from inverting a negative scan?
It models the film rather than just flipping it. The orange mask is removed from each channel's own sensitometry with no camera profiles and no border color picking, the print is built in density space on an H&D curve with toe, straight section and shoulder, and it is graded in ISO-R points with paper profiles from Ilford, Kodak, Foma and Fuji datasheets.
Which film scanners and cameras does NegPy support?
The Plustek OpticFilm 8200i SE, 8100 V2, 8300i SE and 7600i v1 over USB, the Nikon Coolscan, Reflecta, Pacific Image, and anything SANE supports. Tethered camera scanning works on macOS and Linux with narrowband RGB captures, but camera scanning is not available on Windows because the gphoto2 dependency has no Windows build.
How do I run NegPy on Linux?
Make the .AppImage executable with chmod +x and run it, or use nix run github:marcinz606/NegPy. Optionally install libsane for SANE and camera scanning on Debian and Ubuntu, or sane and libgphoto2 on Arch. Everything is stored in Documents/NegPy, and NEGPY_USER_DIR overrides that location.
Does NegPy modify my original scan files?
No, it is non-destructive and source files are never modified. Edits live in a local SQLite database keyed by file content, so files can move, and optional .negpy sidecars can sit next to the scans. Undo history and named work prints survive a restart.
What is NegPy's development status and what does it need to run?
It is classified as 3 - Alpha in the packaging metadata and requires Python 3.13 or newer, with the GPU environment classifier. The release history is fast, with 0.62.0, 0.63.0 and 0.64.0 tagged in the three weeks before 2026-10-11, and the README notes the builds are not signed so a warning on first run is expected.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/marcinz606-negpy)