Сразу сделаю оговорку — «Бюджетным полароидом» я эту штуку называю не потому, что она недорогая (ибо детали суммарно мне обошлись дороже, чем самый дешёвый полароид), а потому что это просто наколенночный проект. Который всё же работает. И при этом вышло, что в долгосрок это обходится дешевле — одна фотка на оригинальный полароид стоит около 1 евро, а на эту камеру — около цента (исходя из стоимости рулона термоленты).

Но мы не будем напрямую сравнивать полученные фото, потому что это совсем разные истории. Итак, давайте про эту поделку.

Де-факто у меня получилась моментальная камера, которая печатает фотографии с помощью термопринтера — как чеки на контрольно-кассовых терминалах. Само собой, качество фотографий не такое высокое, как у самопроявляющейся пленки Polaroid. Но своё, гм, очарование у этого тоже есть. 

Камера

Камера подключена к небольшому компьютеру (я взял Raspberry Pi Zero), который обрабатывает изображение и отправляет его на принтер.

Этого малыша мне хватило – тут есть порты для монитора, клавиатуры и мыши, а ещё Bluetooth и Wi-Fi. Плюс пара контактов, к которым можно припаять светодиоды, кнопки, датчики и запрограммировать всё это добро на выполнение любых действий.

Сама камера совсем простая в использовании.

Камера Raspberry Pi с кабелем
Камера Raspberry Pi с кабелем

Питание

Raspberry Pi нужен источник питания. Конечно, никто не мешает просто взять и подключить его к розетке, но тут мы немного теряем фактор портативности камеры. Можно купить пару аккумуляторов, преобразователь постоянного тока, контроллер заряда аккумулятора и сделать свой собственный перезаряжаемый источник питания.

Ну например
Ну например

Или поступить ещё проще (как я) — купить внешний аккумулятор, в котором есть все вышеперечисленное, и разобрать его. Оно ещё, кстати, заметно дешевле, чем покупать все детали отдельно и собирать их потом.

Тут даже индикатор заряда в процентах есть. Ну, был
Тут даже индикатор заряда в процентах есть. Ну, был

Сразу важное — не забывайте про технику безопасности (особенно защитные очки), ибо если вы будете разбирать павербанк и каким-то острым предметом проткнёте аккумулятор, скорее всего, он загорится. И приятного в этом будет не сильно много. Ну а если вы всё же перестарались, тушите всё это песком, а не водой. 

Разобрал половину и ничего не сжёг
Разобрал половину и ничего не сжёг
А вот аккумулятор без корпуса, всё ещё ничего не горит
А вот аккумулятор без корпуса, всё ещё ничего не горит

Принтер

Смахивает на обычный (просто маленький) принтер, только ему нужна не обычная бумага и чернила, а тепло. 

Подключение — по кабелю или Bluetooth. Вообще, можно упороться и сделать снимок на смартфон, а потом отправить его на принтер по Bluetooth. Но тут уже кому что интереснее.

Про принтеры в целом добавить нечего, я заказал оба из Китая, один за 20 евро, другой — 60. Хотите такой же — используйте для поиска «PT-310».

Корпус

Первым делом я измерил линейкой сам принтер.

Было бы сильно круче, будь он просто квадратным. Но нет же
Было бы сильно круче, будь он просто квадратным. Но нет же

Затем запустил старый добрый FreeCAD, чтобы сделать модель и распечатать ее на 3D-принтере.

Вышло несколько деталек: корпус для принтера, корпус для аккумулятора и компьютера. Они должны соединяться, как бутерброд. А, ну и несколько кнопок и декоративных элементов.

Отшлифовал и проверил, как всё держится
Отшлифовал и проверил, как всё держится
Избавился от щелей с помощью грунтовки
Избавился от щелей с помощью грунтовки
Выбрал цвет
Выбрал цвет

Сборка

Как вы видите по количеству деталек, сложной сборку назвать нельзя. Если мы про корпус.

Светодиоды
Светодиоды

Добавил несколько статусных диодов: синий — питание включено, зеленый — камера включена, красный — делается снимок.

На Raspberry Pi нет кнопки «питание», его можно выключить, отсоединив кабель питания, или запрограммировать кнопку так, чтобы при нажатии она выключала устройство. Но дальше тогда встаёт вопрос — как включить его, если нет кнопки питания? Вынуть кабель питания и вставить его обратно, то есть выполнить цикл включения и выключения. 

Меня это не особо устраивало, так что я добавил переключатель, который позволяет подключать кабель питания. Оба переключателя находятся на панели сбоку от камеры.

В общем, у меня получился аналог вот такого процесса.

Кнопка выключения, переключатель в положении ВКЛ
Кнопка выключения, переключатель в положении ВКЛ

На передней панели корпуса есть еще одна кнопка — для съемки фотографий. На фото она в правом верхнем углу, рядом с компьютером, аккумулятором (под пластиковой пластиной, которая удерживает его на месте) и кнопками питания с индикаторами.

Передняя часть камеры
Передняя часть камеры

Да, небольшой бардак с проводами, но по-другому никак. Единственное, что я смог сделать — это промаркировать их по цветам, чтобы понимать, чего куда.

А вот другая сторона корпуса: я просверлил в принтере пару отверстий и прикрепил его к корпусу с помощью винтов.

Здесь тоже есть схема внешнего аккумулятора и кнопка, при нажатии на которую распечатывается последняя сделанная фотография — на случай, если мне понадобится две одинаковые.

Больше проводов богу проводов!
Больше проводов богу проводов!

Два провода для кнопки, четыре — для питания и два для того, что я нашел в павербанке, — термодатчика. Если аккумулятор перегревается, срабатывает датчик и питание отключается, чтобы не спалить дом. Что вообще неплохо.

Как заставить это работать

Python. Потому что с его помощью можно работать со всеми доступными библиотеками для Raspberry Pi, камерой и функциями обработки изображений. 

Если вам интересно, то вот код.

Много кода
from PIL import Image, ImageEnhance
from escpos.printer import Usb
from picamera2 import Picamera2
import RPi.GPIO as GPIO
import cv2
import numpy as np
import os
import signal
import threading
import time
import io

shots_dir = './shots'
last_image = None
os.makedirs(shots_dir, exist_ok=True)

camera_busy = False
reprint_busy = False

PRINTER_VENDOR_ID = 0x28e9
PRINTER_PRODUCT_ID = 0x0289

GPIO.cleanup()

POWER_LED_PIN = 23
CAMERA_LED_PIN = 4
POWER_BUTTON_PIN = 22
CAMERA_BUTTON_PIN = 27
REPRINT_BUTTON_PIN = 26

GPIO.setmode(GPIO.BCM)
GPIO.setup(POWER_LED_PIN, GPIO.OUT)
GPIO.setup(CAMERA_LED_PIN, GPIO.OUT)
GPIO.setup(POWER_BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(CAMERA_BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(REPRINT_BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

picam = Picamera2()

camera_config = picam.create_still_configuration({"size": (2000, 2000)})
picam.align_configuration(camera_config)
picam.configure(camera_config)
picam.set_controls({"FrameRate": 1})
picam.start()

GPIO.output(CAMERA_LED_PIN, GPIO.LOW)
GPIO.output(POWER_LED_PIN, GPIO.HIGH)

def get_image_brightness(image):
    grayscale = image.convert('L')
    histogram = grayscale.histogram()
    pixels = sum(histogram)
    brightness = sum(i * count for i, count in enumerate(histogram)) / pixels
    return brightness

def histogram_equalization(image):
    img_gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)
    equalize_hist = cv2.equalizeHist(img_gray)
    equalized_image = Image.fromarray(equalize_hist)
    return equalized_image

def gamma_correction(image, gamma=0.7):
    img_array = np.array(image) / 255.0
    corrected_array = np.power(img_array, gamma)
    corrected_image = Image.fromarray((corrected_array * 255).astype('uint8'))
    return corrected_image

def apply_clahe(image):
    img_gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
    clahe_applied = clahe.apply(img_gray)
    equalized_image = Image.fromarray(clahe_applied)
    return equalized_image

def contrast_stretch(image, low=50, high=200):
    img_array = np.array(image)
    min_val = np.min(img_array)
    max_val = np.max(img_array)
    
    stretched = (img_array - min_val) * ((high - low) / (max_val - min_val)) + low
    stretched = np.clip(stretched, 0, 255)
    stretched_image = Image.fromarray(stretched.astype('uint8'))
    return stretched_image

def camera_button_callback(channel):
    global last_image, camera_busy

    if camera_busy:
        return

    camera_busy = True

    def job():
        global last_image, camera_busy

        try:
            GPIO.output(CAMERA_LED_PIN, GPIO.HIGH)
            timestamp = int(time.time())
            filename = f"shots/image_{timestamp}.jpg"

            picam.capture_file(filename)

            GPIO.output(CAMERA_LED_PIN, GPIO.LOW)

            image = Image.open(filename)
            image = image.resize((576, int(image.height * 576 / image.width)))

            brightness = get_image_brightness(image)

            if brightness < 60:
                image = histogram_equalization(image)
            elif brightness < 90:
                image = gamma_correction(image)
            elif brightness < 110:
                image = apply_clahe(image)
            elif brightness < 130:
                image = gamma_correction(image)
            else:
                image = contrast_stretch(image)

            converted_filename = f"shots/image_{timestamp}_converted.jpg"
            image.save(converted_filename)

            last_image = io.BytesIO()
            image.save(last_image, format='JPEG')
            last_image.seek(0)

            printer = Usb(PRINTER_VENDOR_ID, PRINTER_PRODUCT_ID) 
            printer.image(converted_filename)
            printer.textln()
            printer.textln()
            printer.close()

            os.remove(converted_filename)
        except Exception as e:
            print(f"Error during capture/print: {e}")

        finally:
            camera_busy = False

    threading.Thread(target=job, daemon=True).start()

def power_button_callback(channel):
    os.system('shutdown now')

def reprint_button_callback(channel):
    global last_image, reprint_busy

    if reprint_busy:
        return

    reprint_busy = True

    def job():
        global last_image, reprint_busy

        try:
            if last_image is None:
                return
            
            GPIO.output(CAMERA_LED_PIN, GPIO.HIGH)

            printer = Usb(PRINTER_VENDOR_ID, PRINTER_PRODUCT_ID) 
            printer.image(Image.open(last_image))
            printer.textln()
            printer.textln()
            printer.close()

            GPIO.output(CAMERA_LED_PIN, GPIO.LOW)
        finally:
            reprint_busy = False

    threading.Thread(target=job, daemon=True).start()

def cleanup():
    GPIO.output(CAMERA_LED_PIN, GPIO.HIGH)
    GPIO.cleanup()
    picam.close()

GPIO.add_event_detect(CAMERA_BUTTON_PIN, GPIO.FALLING, callback=camera_button_callback, bouncetime=200)
GPIO.add_event_detect(POWER_BUTTON_PIN, GPIO.FALLING, callback=power_button_callback, bouncetime=200)
GPIO.add_event_detect(REPRINT_BUTTON_PIN, GPIO.FALLING, callback=reprint_button_callback, bouncetime=200)

stop_event = threading.Event()

def worker():
    while not stop_event.is_set():
        time.sleep(1)

def handle_signal(signal_num, frame):
    cleanup()
    stop_event.set()

signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)

thread = threading.Thread(target=worker, daemon=True)
thread.start()

stop_event.wait()

Камера Pi в целом-то неплохая, но иногда изображение получается слишком темным, а иногда — слишком светлым. Поэтому в приведенном выше коде вы можете увидеть набор функций, которые регулируют яркость изображения.

Разные настройки на одной и той же фотографии
Разные настройки на одной и той же фотографии

Методом проб и ошибок я выяснил, когда какую настройку использовать.

А вот он в действии.

Комментарии (4)


  1. zionweeds
    17.04.2026 11:26

    Крутяк;))


  1. Sanchezko
    17.04.2026 11:26

    Наконец-то пахнуло настоящим исследовательским Хабром.


  1. Moog_Prodigy
    17.04.2026 11:26

    Уже была подобная детская камера, тоже с термопринтером и чековой лентой. Дешево. https://habr.com/ru/articles/856610/


    1. tormozedison
      17.04.2026 11:26

      Да, это продаётся готовое. Но на коленке сделанное всё равно интересно.

      Вот как на коленке сделать, чтобы изображение на термобумаге не исчезало через несколько месяцев. Кроме, как обвести вручную (тоже можно), способы не придумываются.