Melukis Cahaya dengan LED Tunggal: Buat Gambar Dinamis dengan Sistem Motor DIY
Komponen dan Perlengkapan
| Motor Stepper NEMA17 | 2 |
| Driver Theremino DRV8825 untuk Motor Stepper Sistem Theremino | 2 |
| Pembaca Kartu SD | 1 |
| Arduino Mega 2560 | 1 |
| Berbagai timing belt dan v-wheels | 1 |
Alat dan Mesin yang Diperlukan
Printer 3D (generik)
Aplikasi dan Layanan Online
Arduino IDE, GIMP, Autodesk Fusion 360
Tentang Proyek Ini
Ide
Setelah menonton beberapa video dan melihat banyak artikel tentang light painting, saya mencoba melakukannya sendiri. Light painting melibatkan penggunaan kamera dengan waktu eksposur sangat lama untuk menangkap sumber cahaya kecil. Ini memungkinkan satu cahaya menjadi garis panjang dalam satu gambar.
Tetapi bagaimana jika seseorang ingin membuat gambar yang lebih detail atau menggunakan banyak warna berbeda? Inilah bagaimana saya mendapatkan ide untuk membangun mesin CNC 2 sumbu yang memiliki satu LED RGB yang dapat berubah warna dan "melukis" gambar.
Rencana
Proyek ini membutuhkan empat komponen utama untuk bekerja: mesin CNC 2 sumbu, LED RGB, kartu SD, dan kamera yang mampu mengambil foto eksposur panjang. Pertama, Arduino Mega akan membaca kartu SD dan menemukan bitmap untuk dicetak.
Kemudian, mesin akan bergerak secara horizontal dan menyala kan LED yang sesuai sambil juga turun satu baris setiap kali lebar gambar tercapai. Terakhir, ia akan menunggu sebentar lalu mencari bitmap berikutnya, dan akhirnya berhenti ketika tidak ada gambar lagi yang akan dibuat.
Membangun Rangka
Karena pengalaman saya merancang dan membangun mesin CNC, langkah ini tidak terlalu sulit. Saya ingin membuat sesuatu yang modular yang juga dapat diperluas untuk proyek lain, jadi saya menetapkan desain sederhana yang menggunakan dua timing belt yang terpasang pada crossbar yang bergerak di sepanjang profil aluminium paralel. Ini memungkinkan panjang setiap sumbu sangat dapat disesuaikan. Ujung sumbu X memiliki tutup ujung hasil cetak 3D, salah satu dilengkaki dengan montir untuk motor stepper sumbu X dan bearing.
Membaca Bitmap
Saya memilih format file bitmap karena kesederhanaannya dan seberapa mudah dibacanya. Berdasarkan format file, ada beberapa alamat penting dalam file itu sendiri yang harus dibaca. Ini adalah 0x12 (lebar), 0x16 (tinggi), 0x1C (kedalaman warna), 0xA (lokasi data piksel), dan akhirnya 0x36 (di mana data piksel biasanya berada). Data dibaca dalam potongan yang terdiri dari dua atau empat byte (16 atau 32 bit), yang juga memajukan pointer ke alamat berikutnya. Fungsi baca melalui dan mengambil semua data penting, termasuk offset dan ukuran. Kemudian ia melalui dan membaca setiap piksel, baris per baris.
Menyiapkan Gambar
Karena sebagian besar kamera terbatas pada waktu eksposur maksimum 30 detik, ada batas sekitar 288 total piksel yang dapat ditampilkan dalam waktu tersebut. Ini setara dengan gambar sekitar 18 x 16. Untuk membuat gambar saya, saya membuka GIMP dan mulai membuat pixel art yang sangat sederhana. Ini termasuk bola Poké, hati, dan Mario yang melompat. Kemudian saya menempatkan tiga gambar ini ke dalam direktori yang disebut "bitmaps" di direktori root kartu SD. Program membaca semua gambar dari folder ini.
Program Melukis
Karena motor stepper tidak memiliki sistem umpan balik posisi internal, posisinya harus dilacak oleh perangkat lunak. Program yang saya tulis melacak posisi LED dengan sistem grid untuk memungkinkan penskalaan yang mudah. Ketika Arduino Mega dinyalakan, posisi stepper diatur ke 0, 0 dan kemudian gambar pertama ditemukan dan dibaca. Kemudian, LED berkedip lima kali untuk memberi tahu penggemar foto bahwa hampir waktunya untuk mulai menangkap. Bitmap dibaca dengan terlebih dahulu mengulangi setiap baris, dan dalam setiap baris, setiap kolom dibaca. Dengan mengetahui baris dan kolom saat ini, motor stepper dapat dipindahkan ke posisi yang sama. Pada setiap posisi, LED diubah menjadi warna piksel yang sesuai.
(Membuat Ulang) Gambar
Setelah memasukkan kartu SD dan menyambungkan sumber daya 12V untuk motor, saatnya untuk menyala mesin. Pada kamera saya, saya mengatur waktu eksposur 20 detik, diafragma F36, ISO 100, dan kompensasi eksposur -5 stops untuk meminimalkan efek hantu. Gambar pertama yang digambar adalah bola Poké, terlihat di sini:
Meskipun agak kabur, bentuknya masih dapat terlihat dengan jelas. Kemudian ia membuat bitmap hati:
Karena gambar ini hanya berukuran 9 x 9 piksel, setiap piksel individual kurang terdefinisi. Terakhir, saya melukis gambar Mario yang melompat:
Gambar ini memiliki hantu yang berat, terutama karena banyaknya piksel berwarna cerah.
Ide Masa Depan untuk Peningkatan
Lukisan cahaya yang saya buat ternyata jauh lebih baik dari yang saya kira pada awalnya, tetapi masih ada ruang untuk peningkatan. Hal utama yang ingin saya lakukan adalah mengurangi jumlah keburaman dengan membuat LED bergerak saat padam lalu hanya menyala saat diam. Teknik ini akan sangat meningkatkan kejelasan gambar yang dibuat ulang.
Kode
Program Melukis Cahaya
//Bitmap reading function partially from Adafruit #include <SD.h> #include <SPI.h> #include "DRV8825.h" #define MOTOR_STEPS 200 #define RPM 150 #define MICROSTEPS 4 //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 SPACE_BETWEEN_POSITIONS 5 //5mm per move #define R A0 #define G A1 #define B A2 #define SD_CS 22 int currentPositions[] = {0, 0}; 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); void setup() { Serial.begin(115200); init_steppers(); SD.begin(SD_CS); createBitmaps(); stepperX.disable(); stepperY.disable(); while(1); } void loop() { } void createBitmaps(){ File dir = SD.open("bitmaps"); while(true){ File bitmap = dir.openNextFile(); if(!bitmap){ break; } paintBitmap(bitmap); delay(15000); } } #define BUFFPIXEL 20 void paintBitmap(File bmpFile){ int bmpWidth, bmpHeight; uint8_t bmpDepth; uint32_t bmpImageOffset; uint32_t rowSize; // Not always = bmpWidth; may have padding uint8_t sdbuffer[3 * BUFFPIXEL]; // pixel buffer (R+G+B per pixel) uint8_t buffidx = sizeof(sdbuffer); // Current position in sdbuffer boolean goodBmp = false; // Set to true on valid header parse boolean flip = true; // BMP is stored bottom-to-top int w, h, row, col; uint8_t r, g, b; uint32_t pos = 0, startTime = millis(); Serial.println(); Serial.print("Loading image '"); Serial.print(bmpFile.name()); Serial.println('\''); // Open requested file on SD card // Parse BMP header if (read16(bmpFile) == 0x4D42) { // BMP signature Serial.print("File size: "); Serial.println(read32(bmpFile)); (void)read32(bmpFile); // Read & ignore creator bytes bmpImageOffset = read32(bmpFile); // Start of image data Serial.print("Image Offset: "); Serial.println(bmpImageOffset, DEC); // Read DIB header Serial.print("Header size: "); Serial.println(read32(bmpFile)); bmpWidth = read32(bmpFile); bmpHeight = read32(bmpFile); if (read16(bmpFile) == 1) { // # planes -- must be '1' bmpDepth = read16(bmpFile); // bits per pixel Serial.print("Bit Depth: "); Serial.println(bmpDepth); if ((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed goodBmp = true; // Supported BMP format -- proceed! Serial.print("Image size: "); Serial.print(bmpWidth); Serial.print('x'); Serial.println(bmpHeight); // BMP rows are padded (if needed) to 4-byte boundary rowSize = (bmpWidth * 3 + 3) & ~3; // If bmpHeight is negative, image is in top-down order. // This is not canon but has been observed in the wild. if (bmpHeight < 0) { bmpHeight = -bmpHeight; flip = false; } // Crop area to be loaded w = bmpWidth; h = bmpHeight; if(bmpWidth*bmpHeight>290){ //Too large Serial.println("File is too large to be printed."); return; } for(uint8_t i=0; i<5;i++){ analogWrite(R, 150); delay(500); analogWrite(R, 0); delay(500); } for (row = 0; row<h; row++) { // For each scanline... moveToPosition(0, row); // Seek to start of scan line. It might seem labor- // intensive to be doing this on every line, but this // method covers a lot of gritty details like cropping // and scanline padding. Also, the seek only takes // place if the file position actually needs to change // (avoids a lot of cluster math in SD library). if (flip) // Bitmap is stored bottom-to-top order (normal BMP) pos = bmpImageOffset + (bmpHeight - 1 - row) * rowSize; else // Bitmap is stored top-to-bottom pos = bmpImageOffset + row * rowSize; if (bmpFile.position() != pos) { // Need seek? bmpFile.seek(pos); buffidx = sizeof(sdbuffer); // Force buffer reload } // optimize by setting pins now for (col = 0; col<w; col++) { // For each pixel... // Time to read more pixel data? if (buffidx >= sizeof(sdbuffer)) { // Indeed bmpFile.read(sdbuffer, sizeof(sdbuffer)); buffidx = 0; // Set index to beginning } // Convert pixel from BMP to TFT format, push to display b = sdbuffer[buffidx++]; g = sdbuffer[buffidx++]; r = sdbuffer[buffidx++]; moveToPosition(col, row); activateLED(r,g,b); // optimized! //tft.pushColor(tft.Color565(r,g,b)); } // end pixel analogWrite(R, 0); analogWrite(G, 0); analogWrite(B, 0); } // end scanline Serial.print("Loaded in "); Serial.print(millis() - startTime); Serial.println(" ms"); } // end goodBmp } } bmpFile.close(); moveToPosition(0,0); if (!goodBmp) Serial.println("BMP format not recognized."); } uint16_t read16(File f) { uint16_t result; ((uint8_t *)&result)[0] = f.read(); // LSB ((uint8_t *)&result)[1] = f.read(); // MSB return result; } uint32_t read32(File f) { uint32_t result; ((uint8_t *)&result)[0] = f.read(); // LSB ((uint8_t *)&result)[1] = f.read(); ((uint8_t *)&result)[2] = f.read(); ((uint8_t *)&result)[3] = f.read(); // MSB return result; } void activateLED(int r, int g, int b){ Serial.print(F("LED has value of: ")); Serial.print(r); Serial.print(", "); Serial.print(g); Serial.print(", "); Serial.println(b); analogWrite(R, r); analogWrite(G, g); analogWrite(B, b); } void moveToPosition(int x, int y){ int newPosX = (x-currentPositions[X])*STEPS_PER_MM*X_DIR_FLAG*SPACE_BETWEEN_POSITIONS; int newPosY = (y-currentPositions[Y])*STEPS_PER_MM*Y_DIR_FLAG*SPACE_BETWEEN_POSITIONS; 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_steppers(){ stepperX.begin(RPM); stepperX.setEnableActiveState(LOW); stepperX.enable(); stepperX.setMicrostep(MICROSTEPS); stepperY.begin(RPM); stepperY.setEnableActiveState(LOW); stepperY.enable(); stepperY.setMicrostep(MICROSTEPS); }
Bagian dan Enclosure Khusus
Skematik