Skip to main content

Python image processing package for converting, downscaling, and optionally upscaling images using Pillow.

Project description

imgtools_m8

Python package PyPI package codecov Codacy Badge Downloads Known Vulnerabilities

Python image processing package for converting, downscaling, and optionally upscaling images using Pillow. DNN-based super-resolution upscaling (via OpenCV) is available as an optional extra.

Installation

# Core install (Pillow + Pydantic + NumPy only)
pip install imgtools_m8 --upgrade

# With DNN upscaling support (adds opencv-contrib-python)
pip install "imgtools_m8[dnn]" --upgrade

# With CLI color output (adds colorama)
pip install "imgtools_m8[cli]" --upgrade

# Everything at once
pip install "imgtools_m8[dnn,cli]" --upgrade

# From GitHub
pip install "git+https://github.com/mano8/imgtools_m8" --upgrade

Dependencies

Package Required Notes
Pillow>=12.2.0 yes Core image I/O and format conversion
pydantic>=2.13.4 yes Config validation
numpy>=2.4.6 yes Array support
opencv-contrib-python>=4.13.0.92 no DNN upscaling only — pip install imgtools_m8[dnn]
colorama>=0.4.6 no Colored CLI output — pip install imgtools_m8[cli]

Quick start

from imgtools_m8.image_process import ImageProcessing

obj = ImageProcessing(
    conf={
        "source_path": "./tests/sources_test/recien_llegado.jpg",
        "output_path": "./output",
        "output_options": [
            {
                "formats": [
                    {"ext": "JPEG", "quality": 80, "progressive": True, "optimize": True},
                    {"ext": "WEBP", "quality": 70},
                    {"ext": "PNG"},
                ]
            }
        ],
    }
)
obj.run()

Usage

ImageProcessing is the main class. It accepts a conf dict validated by ImageProcessingSchema.

Configuration structure

conf = {
    # Required
    "source_path": "/path/to/image.jpg",   # or a directory
    "output_path": "/path/to/output/",

    # Optional
    "include_subdirs": False,   # scan subdirectories when source is a dir
    "flatten_output": False,    # write all outputs flat (no subdir mirror)

    # At least one of output_options or global_options is required
    "output_options": [...],    # per-size output rules (see below)
    "global_options": {...},    # fallback formats/byte-limit for all options
}

output_options entries

Each entry in output_options may specify:

Field Type Description
image_size OutputSize Resize spec (see below)
allow_upscale bool Allow upscaling when image is smaller than target
max_byte_size int Hard byte ceiling per output file (binary-search on quality)
formats list[FormatConfig] Output formats for this size

image_size variants (mutually exclusive where noted)

Field Description
fixed_width Resize to exact width, keep aspect ratio
fixed_height Resize to exact height, keep aspect ratio
fixed_width + fixed_height Fit within bounding box, keep aspect ratio
fixed_size Constrain longest side to N pixels
fixed_downscale Divide each dimension by factor (2–10)
fixed_upscale Multiply each dimension by factor (2–10); uses DNN model when available

Supported output formats

ext value Notes
"JPEG" quality, optimize, progressive, subsampling
"WEBP" quality, lossless, method
"PNG" optimize, compression_level, interlace
"GIF" optimize
"AVIF" quality, lossless

Example 1 — convert to multiple formats without resizing

from imgtools_m8.image_process import ImageProcessing

obj = ImageProcessing(
    conf={
        "source_path": "./tests/sources_test/recien_llegado.jpg",
        "output_path": "./output",
        "output_options": [
            {
                "formats": [
                    {"ext": "JPEG", "quality": 80, "progressive": True, "optimize": True},
                    {"ext": "WEBP", "quality": 70},
                    {"ext": "PNG"},
                ]
            }
        ],
    }
)
obj.run()

Example 2 — downscale to a fixed bounding box

The image is 340×216 px. With fixed_width=300, fixed_height=200, the wider constraint wins (width ratio = 300/340 ≈ 88 %; height ratio = 200/216 ≈ 93 %), so the output is 300×190 px.

from imgtools_m8.image_process import ImageProcessing

obj = ImageProcessing(
    conf={
        "source_path": "./tests/sources_test/recien_llegado.jpg",
        "output_path": "./output",
        "output_options": [
            {
                "image_size": {"fixed_width": 300, "fixed_height": 200},
                "formats": [
                    {"ext": "JPEG", "quality": 80, "progressive": True, "optimize": True}
                ],
            }
        ],
    }
)
obj.run()

Example 3 — upscale then downscale (DNN model)

Requires pip install imgtools_m8[dnn]. The EDSR model (included) is used automatically.

from imgtools_m8.image_process import ImageProcessing

obj = ImageProcessing(
    conf={
        "source_path": "./tests/sources_test/recien_llegado.jpg",
        "output_path": "./output",
        "output_options": [
            {
                "image_size": {"fixed_width": 1200},
                "allow_upscale": True,
                "formats": [
                    {"ext": "JPEG", "quality": 80, "progressive": True, "optimize": True}
                ],
            }
        ],
    }
)
obj.run()

Example 4 — process a whole directory with subdirectory mirroring

from imgtools_m8.image_process import ImageProcessing

obj = ImageProcessing(
    conf={
        "source_path": "./tests/sources_test/",
        "output_path": "./output",
        "include_subdirs": True,
        "output_options": [
            {
                "image_size": {"fixed_size": 800},
                "max_byte_size": 200_000,
                "formats": [
                    {"ext": "WEBP", "quality": 85}
                ],
            }
        ],
    }
)
obj.run()

Example 5 — multiprocessing batch with resource monitoring

from imgtools_m8.multiprocess import MultiProcessImage

obj = MultiProcessImage(
    conf={
        "source_path": "./tests/sources_test/",
        "output_path": "./output",
        "include_subdirs": True,
        "output_options": [
            {
                "image_size": {"fixed_width": 800},
                "formats": [{"ext": "WEBP", "quality": 80}],
            }
        ],
    },
    max_cpu_percent=75,
    user_cpu_percent=50,
    batch_size=32,
)
obj.run_multiple()

CLI

After installation, an imgtools command is available:

# Convert to WEBP at 80% quality (default when no --format is given)
imgtools --source ./images --output ./out

# Resize to 1920 px wide and save as JPEG + WEBP
imgtools --source ./images --output ./out --width 1920 --format jpg:95 --format webp:80

# Process subdirectories in parallel with 4 workers
imgtools --source ./images --output ./out --subdirs --workers 4

# Load a full conf dict from a JSON file
imgtools --source ./images --output ./out --config ./my_conf.json

# Enable debug logging
imgtools --source ./images --output ./out --debug

Install [cli] to get colored log output:

pip install "imgtools_m8[cli]"

Run imgtools --help for the full argument reference. The --workers flag switches from single-process (ImageProcessing) to multiprocess (MultiProcessImage); the worker count is clamped to cpu_count - 1 to avoid saturating the system.

In-memory API

Process images without any file I/O using process_image:

from imgtools_m8 import process_image

with open("photo.jpg", "rb") as f:
    src_bytes = f.read()

results = process_image(
    src_bytes,
    [{"image_size": {"fixed_width": 200}, "formats": [{"ext": "WEBP", "quality": 80}]}],
)

for r in results:
    print(r.name, r.width, r.height, r.size_bytes)
    # write r.data to a file or upload directly

process_image returns a list[VariantResult]; each result carries name, data (raw bytes), width, height, size_bytes, and format. No disk paths required.

Async / FastAPI usage

process_image is synchronous and CPU-bound (Pillow/OpenCV decode, resize, encode). Calling it directly from an async def route blocks the event loop for the whole encode, starving every other in-flight request. The async wrappers offload that work to a worker thread so the loop stays responsive:

from imgtools_m8 import process_image_async, process_images_async

@app.post("/resize")
async def resize(file: UploadFile):
    src_bytes = await file.read()
    results = await process_image_async(
        src_bytes,
        [{"image_size": {"fixed_width": 200}, "formats": [{"ext": "WEBP", "quality": 80}]}],
    )
    return {"variants": [r.name for r in results]}

@app.post("/resize-batch")
async def resize_batch(files: list[UploadFile]):
    sources = [await f.read() for f in files]
    # one list[VariantResult] per source, order preserved
    batches = await process_images_async(
        sources,
        [{"image_size": {"fixed_width": 64}, "formats": [{"ext": "JPEG"}]}],
    )
    return {"count": len(batches)}

A few things worth knowing:

  • What async buys you: the image work is still CPU-bound. await process_image_async(...) does not make encoding asynchronous — it runs the existing synchronous pipeline in a worker thread so it no longer blocks the event loop. The execution location changed, not the work. Because Pillow/OpenCV release the GIL during encode/decode/resize, this also gives real parallelism for concurrent requests.

  • Thread-pool behavior: the work runs on the event loop's default thread pool, which has a bounded worker count managed by the loop. process_images_async([...]) schedules every source via asyncio.gather, but the executor caps how many run at once — passing thousands of sources creates thousands of awaitables, not thousands of OS threads.

  • Cancellation: cancelling the awaiting task does not stop image processing already running in a worker thread (standard asyncio.to_thread behavior).

  • Heterogeneous options: process_images_async applies the same output_options to every source. For per-image differing options, compose asyncio.gather over process_image_async directly:

    import asyncio
    
    batches = await asyncio.gather(
        process_image_async(img1, opts1),
        process_image_async(img2, opts2),
    )
    
  • Security boundary: these wrappers do not alter image validation, decoding, decompression-bomb protections, or upload-size controls. Input validation and request limits remain the caller's responsibility — see Pillow's Image.MAX_IMAGE_PIXELS and your framework's upload-size caps.

DNN upscaling note

When opencv-contrib-python is not installed, fixed_upscale falls back to PIL bicubic scaling. Install the [dnn] extra to enable DNN upscaling:

pip install "imgtools_m8[dnn]"

Models (DNN upscaling)

The EDSR .pb models are not bundled in the wheel (saves ~111 MB). After installing the [dnn] extra, fetch them once with:

imgtools download-models

Models are SHA256-verified and stored in the platform cache directory. To use a custom location, set IMGTOOLS_M8_MODELS_DIR to a directory that contains an opencv/ subdirectory with the .pb files. If the models are absent when upscaling is attempted, a ModelNotFoundError is raised with a reminder to run imgtools download-models.

Custom models in .pb format can be loaded by passing a model_conf dict to ImageProcessing:

obj = ImageProcessing(
    conf={...},
    model_conf={
        "path": "/path/to/model/directory",
        "model_name": "espcn",   # model prefix, e.g. espcn, edsr, lapsrn
        "scale": 4,              # fixed scale (omit for AUTO_SCALE)
    },
)

Docker (CUDA-accelerated DNN upscaling)

A Dockerfile is provided to build a CUDA-enabled image that compiles OpenCV from source with GPU support, enabling hardware-accelerated DNN upscaling.

Requirements: Docker + NVIDIA Container Toolkit.

# Build with defaults (CUDA 13.3 / Ubuntu 24.04 / OpenCV 4.13)
docker build -t imgtools_m8 .

# Target a specific GPU compute capability (much faster compile)
docker build --build-arg CUDA_ARCH_BIN=8.9 -t imgtools_m8 .

# Run with GPU access
docker run --gpus all imgtools_m8 --help

Find your GPU's compute capability at developer.nvidia.com/cuda-gpus: RTX 30xx → 8.6, RTX 40xx → 8.9, RTX 50xx → 10.0.

Build arguments (OPENCV_VERSION, CUDA_ARCH_BIN) are documented in .env.example. A docker-compose.yml for multi-volume GPU batch processing is in docker_compose/imgtools_dev/.

Input/Output Example

Input Image

The source file is 340×216 px.

Recien Llegado @Cezar llañez

Recien llegado by @Cezar yañez

License

This project is licensed under the Apache 2 License — see the LICENSE file for details.

Authors

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

imgtools_m8-2.1.0.tar.gz (72.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

imgtools_m8-2.1.0-py3-none-any.whl (58.0 kB view details)

Uploaded Python 3

File details

Details for the file imgtools_m8-2.1.0.tar.gz.

File metadata

  • Download URL: imgtools_m8-2.1.0.tar.gz
  • Upload date:
  • Size: 72.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for imgtools_m8-2.1.0.tar.gz
Algorithm Hash digest
SHA256 97b4122f3f6c75028c0c12f10711d2db2ddf51c9d2862fa930395769ff295b7f
MD5 65c2e2e43bb84528120afc7e603e06a7
BLAKE2b-256 5f3b66e6110c4758b22fc15f81bcc74735a21b677fd0f519ab1b66a9e1ca590f

See more details on using hashes here.

File details

Details for the file imgtools_m8-2.1.0-py3-none-any.whl.

File metadata

  • Download URL: imgtools_m8-2.1.0-py3-none-any.whl
  • Upload date:
  • Size: 58.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for imgtools_m8-2.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9b89d46f0f1309233dfac708208211d9bb45571ac4aa376ae99bdb9626525eae
MD5 6fa3a94660ed68561cda47e08f37ad91
BLAKE2b-256 e1dc9b2aae897e4a1f1f5ac1dbe87f0c021b3af363855561c0f170c3e4b91911

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page