Skip to content
egxnPublic

About

🎞️ πŸ“· 🦝 Film scanner using a imx4777 + Raspberry pi + manual Lens

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Neganuki Film Scanner

Modular film‑scanning system built on Raspberry Pi + IMX477 + manual lens + stepper motor, designed with a pipeline approach using:

  • Python backend (independent, headless‑friendly)
  • gRPC for communication
  • State machine using transitions
  • Stepper control for advancing film
  • Optional UIs: local screen on the Pi, mobile/remote UI, or keyboard‑only operation
  • Poetry for dependency and environment management

Project Structure

neganuki/
β”‚
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ camera/              # Camera control (IMX477 with Picamera2)
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   └── imx477.py
β”‚   β”œβ”€β”€ motor/               # Stepper motor control (28BYJ-48)
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   └── stepper.py
β”‚   β”œβ”€β”€ fsm/                 # Finite State Machine (transitions)
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ scanner_fsm.py
β”‚   β”‚   └── states.yaml
β”‚   β”œβ”€β”€ grpc/                # gRPC service
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ server.py
β”‚   β”‚   β”œβ”€β”€ scanner.proto
β”‚   β”‚   β”œβ”€β”€ codegen.py
β”‚   β”‚   └── generated/
β”‚   β”œβ”€β”€ pipeline/            # Core scanning pipeline
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ controller.py    # Main orchestrator
β”‚   β”‚   β”œβ”€β”€ evaluator.py     # Frame quality & overlap analysis
β”‚   β”‚   β”œβ”€β”€ stitcher.py      # Image stitching
β”‚   β”‚   └── crop.py          # Frame cropping
β”‚   └── utils/               # Helpers
β”‚
β”œβ”€β”€ client/                  # UI clients (web / mobile / terminal)
β”‚
β”œβ”€β”€ pyproject.toml           # Poetry configuration
└── README.md

Installing and Using Poetry to Handle Python Dependencies

1. Install Poetry

curl -sSL https://install.python-poetry.org | python3 -

Verify installation:

poetry --version

2. Install Project Dependencies

At the project root:

poetry install

This creates a virtual environment and installs all dependencies including:

  • picamera2 - IMX477 camera control
  • rpi-lgpio - GPIO control for Raspberry Pi
  • opencv-python - Image processing
  • transitions - State machine
  • grpcio & grpcio-tools - gRPC communication
  • rawpy, tifffile - RAW image handling
  • loguru - Logging

3. Activate the Virtual Environment

poetry shell

Or run commands inside it without activating:

poetry run python backend/grpc/server.py

4. Adding Dependencies

Example:

poetry add package-name

For optional dependencies:

poetry install --extras "camera gpio"

For GPIO access, the code imports RPi.GPIO.

On Raspberry Pi 3B with Debian Trixie, the safest option is the Debian package python3-rpi.gpio, which also exposes RPi.GPIO.

If you prefer rpi-lgpio, note that it depends on lgpio and must not be installed in the same Python environment as rpi-gpio / RPi.GPIO.


Generating gRPC Code

Generate Python stubs from proto files:

poetry run python -m grpc_tools.protoc \
  -I./backend/grpc \
  --python_out=./backend/grpc/generated \
  --grpc_python_out=./backend/grpc/generated \
  ./backend/grpc/scanner.proto

Or use the built-in script:

poetry run generate-protos

Running the Backend

Start the gRPC server and the MJPEG live preview with a single command:

poetry run neganuki-server

This starts:

  • gRPC server on 0.0.0.0:50051
  • MJPEG preview at http://<pi-ip>:8080/ (open in any browser)

Custom ports / options:

poetry run neganuki-server --host 0.0.0.0 --port 50051 --http-port 8080

Disable the browser preview:

poetry run neganuki-server --no-preview

Capturing and Copying Photos to Your PC

Run from your PC (not the Pi). Requires SSH access to the Pi.

poetry run python clients/neganuki-terminal/scanner_client.py \
  --host 192.168.1.13 \
  --action capture --raw \
  --copy-to-host \
  --copy-user <pi-username> \
  --copy-path ~/neganuki/
Option Description
--host Pi's IP address
--raw Capture RAW TIFF instead of RGB preview
--copy-to-host Pull the file to your PC via scp after capture
--copy-user SSH username on the Pi
--copy-path Local destination folder on your PC

Omit --raw to capture an RGB frame instead.


Running the Client UI

Graphical User Interface (Tkinter)

The GUI client provides a complete interface with scan controls, live preview, and status monitoring.

Installation

Ensure Pillow is installed for image handling:

poetry add Pillow

Running the GUI

poetry run python client/neganuki-ui/scanner_gui.py

Or from within the poetry shell:

poetry shell
cd client/neganuki-ui
python scanner_gui.py

GUI Features

  • Connection Management - Connect to scanner with host/port configuration
  • Scan Controls - Start, pause, resume, and stop scanning operations
  • Frame Capture - Capture single RGB or RAW frames
  • Live Preview - Real-time camera preview when scanner is idle (10 FPS)
  • Status Monitoring - Auto-refresh status with frame count and state tracking
  • Preview Display - View captured frames and live camera feed
  • Status Log - Scrollable log with timestamps and color-coded messages

Quick Start

  1. Start the backend server (in one terminal):

    poetry run python backend/grpc/server.py
  2. Start the GUI client (in another terminal):

    poetry run python client/neganuki-ui/scanner_gui.py
  3. In the GUI:

    • Enter host: localhost (or Pi IP address if remote)
    • Enter port: 50051
    • Click Connect
    • Check Live Preview to see camera feed (when idle)
    • Click Start Scan to begin scanning

Command-Line Clients

Interactive Menu Client

Terminal-based menu interface:

poetry run python client/raspberry-pi/interactive_scanner.py

Features: Interactive menu, status display, pause/resume control, frame capture

Simple Script Client

Automation-friendly script:

# Quick scan
poetry run python client/raspberry-pi/simple_scan.py --quick-scan

# Capture test frame
poetry run python client/raspberry-pi/simple_scan.py --capture

# Monitor scan
poetry run python client/raspberry-pi/simple_scan.py --monitor

Programmatic Client Library

Use the client library in your own scripts:

from client.raspberry_pi.scanner_client import ScannerClient

client = ScannerClient()
client.connect("localhost:50051")

# Start scanning
client.start_scan()

# Monitor progress
for state, frame_count in client.stream_status():
    print(f"State: {state}, Frames: {frame_count}")
    if state == "finished":
        break

client.shutdown()

Hardware Setup

Required Components

  • Raspberry Pi 3B (or newer) running Raspberry Pi OS Bookworm
  • IMX477 Camera Module (12MP, HQ Camera)
  • 28BYJ-48 Stepper Motor with ULN2003 driver
  • Manual lens compatible with IMX477 (C/CS mount)

GPIO Pin Configuration

Default stepper motor pins (configurable):

  • IN1: GPIO 17
  • IN2: GPIO 18
  • IN3: GPIO 27
  • IN4: GPIO 22

Permissions

Add your user to the GPIO group:

sudo usermod -aG gpio $USER

State Machine

The scanner uses a finite state machine with the following states:

States

  • idle - Waiting to start
  • initializing - Setting up camera and motor
  • capturing - Taking a photo
  • evaluating - Checking frame quality
  • stitching - Combining frames
  • advancing - Moving film forward
  • checking_completion - Deciding if scan is done
  • paused - Scan temporarily stopped
  • finished - Scan completed successfully
  • error - General error state
  • camera_error - Camera-specific error (with recovery)
  • motor_error - Motor-specific error (with recovery)

Transitions

  • start - Begin scanning
  • pause / resume - Pause and resume operations
  • retry_capture - Retry if frame quality is poor
  • fail - Enter error state
  • recover_camera / recover_motor - Automatic recovery from errors
  • abort - Emergency stop

gRPC API

Available RPCs

StartCapture

Start the scanning pipeline.

stub.StartCapture(scanner_pb2.CaptureRequest())

GetStatus

Get current FSM state and frame count.

status = stub.GetStatus(scanner_pb2.StatusRequest())
print(f"State: {status.state}, Frames: {status.frame_count}")

PauseScan / ResumeScan

Pause or resume the current scan.

stub.PauseScan(scanner_pb2.PauseRequest())
stub.ResumeScan(scanner_pb2.ResumeRequest())

CaptureFrame

Capture a single frame (bypass FSM).

# Preview frame
response = stub.CaptureFrame(scanner_pb2.FrameCaptureRequest(raw=False))

# RAW frame
response = stub.CaptureFrame(scanner_pb2.FrameCaptureRequest(raw=True))

StreamStatus

Real-time status updates via server streaming.

for update in stub.StreamStatus(scanner_pb2.StatusRequest()):
    print(f"State: {update.state}, Frames: {update.frame_count}")

Shutdown

Stop scanning and cleanup resources.

stub.Shutdown(scanner_pb2.ShutdownRequest())

Configuration

Camera Settings

Configure in PipelineController:

camera_config = {
    'resolution': (4056, 3040),  # IMX477 full resolution
}

controller = PipelineController(
    output_dir="./output",
    camera_config=camera_config,
    max_frames=100,
    detect_film_end=True
)

Motor Settings

motor_pins = {
    'pins': (17, 18, 27, 22),  # GPIO pins
    'delay': 0.002              # Step delay in seconds
}

controller = PipelineController(
    output_dir="./output",
    motor_pins=motor_pins
)

Frame Quality Thresholds

Adjust in CaptureEvaluator:

evaluator = CaptureEvaluator(
    sharpness_threshold=100.0,   # Laplacian variance
    brightness_min=30.0,          # Min acceptable brightness
    brightness_max=225.0,         # Max acceptable brightness
)

Features

Automatic Quality Control

  • Sharpness detection - Rejects blurry frames
  • Exposure validation - Detects over/underexposed frames
  • Retry mechanism - Up to 3 retries per frame

Film End Detection

  • Dark frame detection - Identifies end of film
  • Edge density analysis - Detects blank leader/trailer
  • Frame count limit - Prevents runaway scanning

Error Recovery

  • Camera errors - Automatic reinitialization
  • Motor errors - GPIO reset and recovery
  • Graceful degradation - Continues when possible

Image Stitching

  • Feature-based alignment - ORB or SIFT descriptors
  • Homography transformation - Accurate frame alignment
  • Automatic blending - Seamless mosaic creation

Development Setup

Semantic Commits

This project uses Conventional Commits for consistent commit messages and automatic versioning.

Quick Setup

# Install development dependencies
poetry install --with dev

# Install pre-commit hooks
poetry run pre-commit install
poetry run pre-commit install --hook-type commit-msg

Usage

# Interactive commit (recommended)
git add .
poetry run cz commit

# Manual commit (validated by pre-commit hook)
git commit -m "feat(camera): add live preview streaming"

Version Bumping

# Automatic version bump and changelog generation
poetry run cz bump

# Push with tags
git push --follow-tags origin main

For detailed instructions, see docs/semantic-commits/ and CONTRIBUTING.md.


Optional: Generate requirements.txt

Useful for users who don’t use Poetry:

poetry export -f requirements.txt --output requirements.txt --without-hashes

Notes

  • The backend is designed to run headless (no UI required)
  • Control everything via gRPC from any client
  • The pipeline is modular and extensible
  • Built around the RPi.GPIO API so it can run with either python3-rpi.gpio or rpi-lgpio, depending on the target image
  • Supports both preview (RGB) and RAW (Bayer) capture modes
  • State machine configuration in YAML for easy customization
  • Automatic error recovery for camera and motor failures

State Machine Diagram

stateDiagram-v2
    [*] --> idle

    idle --> initializing: start
    initializing --> capturing: init_done

    capturing --> evaluating: capture_done
    evaluating --> capturing: retry_capture
    evaluating --> stitching: accept_capture

    stitching --> advancing: stitch_done
    advancing --> checking_completion: advance_done

    checking_completion --> capturing: more_frames
    checking_completion --> finished: scan_complete

    capturing --> paused: pause
    evaluating --> paused: pause
    stitching --> paused: pause
    advancing --> paused: pause
    paused --> capturing: resume

    capturing --> camera_error: camera_fail
    evaluating --> camera_error: camera_fail
    camera_error --> capturing: recover_camera

    advancing --> motor_error: motor_fail
    motor_error --> advancing: recover_motor

    idle --> error: fail
    initializing --> error: fail
    capturing --> error: fail
    evaluating --> error: fail
    stitching --> error: fail
    advancing --> error: fail
    checking_completion --> error: fail
    
    camera_error --> error: fail
    motor_error --> error: fail
    error --> idle: recover

    idle --> finished: abort
    capturing --> finished: abort
    evaluating --> finished: abort
    stitching --> finished: abort
    advancing --> finished: abort
    paused --> finished: abort

    finished --> [*]
    error --> [*]
Loading

Troubleshooting

Camera not detected

# Check if camera is connected
vcgencmd get_camera

# Test with libcamera
libcamera-hello

GPIO permission errors

# Add user to gpio group
sudo usermod -aG gpio $USER

# Reboot required
sudo reboot

Import errors

# Ensure virtual environment is activated
poetry shell

# Reinstall dependencies
poetry install --sync

# Install Raspberry Pi GPIO support for this project
poetry install --extras "gpio"

# Or add it explicitly to Poetry
poetry add rpi-lgpio

Raspberry Pi 3B on Debian Trixie

# Recommended on Debian Trixie / Python 3.13
sudo apt install python3-rpi.gpio

The project imports RPi.GPIO, so python3-rpi.gpio works without code changes on a real Raspberry Pi.

If you use rpi-lgpio instead:

# rpi-lgpio needs lgpio too
sudo apt install python3-lgpio
pip install rpi-lgpio

Do not install rpi-lgpio and rpi-gpio in the same Python environment. If you run inside a Poetry virtualenv, remember that system packages such as python3-rpi.gpio are not visible unless that virtualenv is configured to use system site packages.

gRPC connection refused

# Check if server is running
poetry run python backend/grpc/server.py

# Verify port is not in use
netstat -tuln | grep 50051

About

🎞️ πŸ“· 🦝 Film scanner using a imx4777 + Raspberry pi + manual Lens

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages