Evan Rust
Published © GPL3+

Taking a Picture - One Pixel at a Time

A camera that only has a one-pixel sensor. It moves an APDS-9960 across two axes to create a full-color image.

IntermediateFull instructions provided5 hours6,245
Taking a Picture - One Pixel at a Time

Things used in this project

Hardware components

SparkFun APDS-9960
×1
Jumper wires (generic)
Jumper wires (generic)
×1
Arduino Mega 2560
Arduino Mega 2560
×1
NEMA17 Stepper Motor
×2
Adafruit SD card reader
×1
DFRobot Dual Stepper Motor Driver Shield
×1

Software apps and online services

Arduino IDE
Arduino IDE
VS Code
Microsoft VS Code

Hand tools and fabrication machines

Hot glue gun (generic)
Hot glue gun (generic)
3D Printer (generic)
3D Printer (generic)
CNC Router

Story

Read more

Custom parts and enclosures

CNC Machine

Schematics

Motor Driver Pinout

SD Card Wiring

Code

"Camera" Code

C/C++
Run this on the Arduino Mega
#include <Wire.h>
#include <SparkFun_APDS9960.h>
#include <SD.h>
#include <SPI.h>
#include "DRV8825.h"

#define MOTOR_STEPS 200
#define RPM 60
#define MICROSTEPS 32

#define MM_X 60
#define MM_Y 150

//pin definitions
#define STEPPER_X_DIR 7
#define STEPPER_X_STEP 6
#define STEPPER_X_EN 8
#define STEPPER_Y_DIR 4
#define STEPPER_Y_STEP 5
#define STEPPER_Y_EN 12

#define X 0
#define Y 1

#define X_DIR_FLAG -1 //1 or -1 to flip direction
#define Y_DIR_FLAG 1 //1 or -1 to flip direction

#define STEPS_PER_MM (3.75 * MICROSTEPS) //steps needed to move 1mm
#define PIXELS_W 60
#define PIXELS_H 300
#define SPACE_BETWEEN_POSITIONS_X 1
#define SPACE_BETWEEN_POSITIONS_Y .5

#define MULTIPLIER 2 //Scale each color by this amount

#define SD_CS 22

int currentPositions[] = {0, 0};

File image;

DRV8825 stepperX(MOTOR_STEPS, STEPPER_X_DIR, STEPPER_X_STEP, STEPPER_X_EN);
DRV8825 stepperY(MOTOR_STEPS, STEPPER_Y_DIR, STEPPER_Y_STEP, STEPPER_Y_EN);
SparkFun_APDS9960 apds = SparkFun_APDS9960();

uint8_t light_levels[6] = {};
uint16_t red_light = 0;
uint16_t green_light = 0;
uint16_t blue_light = 0;

unsigned long pixel_count = 0;

void setup(){
  Serial.begin(115200);
  SD.begin(SD_CS);
  init_apds();
  init_file();
  take_picture();
}

void loop(){
  
}

void take_picture(){
  unsigned long start_time = millis(); 
  for(int row=0; row<PIXELS_H; row++){
    for(int col=0; col<PIXELS_W; col++){
      moveToPosition(col, row);
      //delay(50);
      readLightLevels();
      writeLightLevels();
      pixel_count++;
      if(pixel_count > 1000){
        image.close();
        image = SD.open("pixelImg.aif", FILE_WRITE);
        pixel_count = 0;
      }
    }
  }
  image.close();
  moveToPosition(0, 0);
  Serial.print("Image finished! It took "); Serial.print((millis() - start_time) / 1000); Serial.println(" seconds to complete.");
}

void writeLightLevels(){
  image.write(light_levels, 3);
}

void readLightLevels(){
  if (  !apds.readRedLight(red_light) ||
        !apds.readGreenLight(green_light) ||
        !apds.readBlueLight(blue_light) ) {
    Serial.println("Error reading light values");
  } else {
    Serial.print("Red: ");
    Serial.print(red_light);
    Serial.print(" Green: ");
    Serial.print(green_light);
    Serial.print(" Blue: ");
    Serial.println(blue_light);
  }
  light_levels[0] = red_light * MULTIPLIER;
  light_levels[1] = green_light * MULTIPLIER;
  light_levels[2] = blue_light * MULTIPLIER;
}

void moveToPosition(int x, int y){
  int newPosX = (x-currentPositions[X])*STEPS_PER_MM*X_DIR_FLAG*SPACE_BETWEEN_POSITIONS_X;
  int newPosY = (y-currentPositions[Y])*STEPS_PER_MM*Y_DIR_FLAG*SPACE_BETWEEN_POSITIONS_Y;
  stepperX.move(newPosX);
  stepperY.move(newPosY);
  currentPositions[X] = x;
  currentPositions[Y] = y;
  Serial.print("Stepper positions: "); Serial.print(currentPositions[X]); Serial.print(", "); Serial.println(currentPositions[Y]);
}

void init_file(){
  if(SD.exists("pixelImg.aif")){
    SD.remove("pixelImg.aif");
  }

  //Data in MSB format
  uint16_t image_w = PIXELS_W;
  uint16_t image_h = PIXELS_H;
  image = SD.open("pixelImg.aif", FILE_WRITE);
  image.write((0xFF00 & image_w) >> 8);
  image.write(0xFF & image_w);
  image.write((0xFF00 & image_h) >> 8);
  image.write(0xFF & image_h);
  image.write(3); //bit-depth of 3 bytes (24-bits)
}

void init_apds(){
  if ( apds.init() ) {
    Serial.println(F("APDS-9960 initialization complete"));
  } else {
    Serial.println(F("Something went wrong during APDS-9960 init!"));
  }
  
  // Start running the APDS-9960 light sensor (no interrupts)
  if ( apds.enableLightSensor(false) ) {
    Serial.println(F("Light sensor is now running"));
  } else {
    Serial.println(F("Something went wrong during light sensor init!"));
  }
  
  // Wait for initialization and calibration to finish
  delay(500);
}

void init_steppers(){
  stepperX.begin(RPM);
  stepperX.setEnableActiveState(LOW);
  stepperX.enable();
  stepperX.setMicrostep(MICROSTEPS);
  stepperY.begin(RPM);
  stepperY.setEnableActiveState(LOW);
  stepperY.enable();
  stepperY.setMicrostep(MICROSTEPS);
}

Python Image Parser

Python
from PIL import Image, ImageDraw

with open("PIXELIMG.AIF", 'rb') as imgFile:
    img_w, img_h = 0, 0
    img_w = int.from_bytes(imgFile.read(2), byteorder='big', signed = False)
    print(img_w)
    img_h = int.from_bytes(imgFile.read(2), byteorder='big', signed = False)
    if int.from_bytes(imgFile.read(1), byteorder='big', signed = False) == 3:
        img = Image.new("RGB", (img_w, img_h))
        for row in range(img_h):
            for col in range(img_w):
                r, g, b = int.from_bytes(imgFile.read(1), byteorder='big', signed = False), int.from_bytes(imgFile.read(1), byteorder='big', signed = False), int.from_bytes(imgFile.read(1), byteorder='big', signed = False)
                img.putpixel((col, row), (r, g, b))
        img.show()
        img.save("finished_img.bmp")

Credits

Evan Rust

Evan Rust

122 projects • 1087 followers
IoT, web, and embedded systems enthusiast. Contact me for product reviews or custom project requests.

Comments