使用一个 LED 创建图像
组件和用品
| × | 2 | ||||
| × | 2 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 |
必要的工具和机器
|
应用和在线服务
| ||||
| ||||
|
关于这个项目
想法
看了几个视频,看了很多关于光绘的文章,我决定试一试。光绘涉及使用具有很长曝光时间的相机来捕捉小光源。这允许单个光在单个图像中串成长条纹。
但是如果有人想要创建更详细的图片或使用许多不同的颜色怎么办?这就是我想出的想法来构建一个 2 轴 CNC 机器,该机器具有单个 RGB LED,可以改变颜色并“绘制”图像。
计划
该项目需要四个主要组件才能工作:一台 2 轴 CNC 机器、RGB LED、SD 卡和一个能够进行长时间曝光拍摄的相机。首先,Arduino Mega 会读取 SD 卡并找到要打印的位图。
然后,它会水平穿过并点亮相应的 LED,同时每次超过图像宽度时也会向下移动一行。最后,它会稍等片刻,然后搜索下一个位图,最后在没有更多图像要创建时停止。
建造钻机
由于我在设计和制造 CNC 机床方面的经验,这一步并不太难。我想做一些模块化的东西,也可以为其他项目扩展,所以我选择了一个简单的设计,使用两个同步带连接到沿着平行铝挤压件移动的横杆上。
这使每个轴的长度可以非常定制。 X 轴的末端有 3D 打印的端盖,其中一个端盖用于安装 X 轴步进电机和轴承。
读取位图
我选择位图文件格式是因为它简单且易于阅读。根据文件格式,文件本身有几个重要地址必须读取。它们是 0x12(宽度)、0x16(高度)、0x1C(颜色深度)、0xA(像素数据的位置),最后是 0x36(像素数据通常所在的位置)。
以两个或四个字节(16 或 32 位)的块读取数据,这也将指针前进到下一个地址。 read 函数遍历并获取所有重要数据,包括偏移量和大小。然后它逐行读取每个像素。
准备图像
由于大多数相机的曝光时间最长为 30 秒,因此在这段时间内可以显示的总像素数限制为大约 288 个。这相当于大约 18 x 16 的图像。为了制作我的图像,我加载了 gimp 并开始创建非常简单的像素艺术。其中包括一个精灵球、一颗心和一个跳跃的马里奥。然后我把这三张图片放到了SD卡根目录下一个叫“bitmaps”的目录下。该程序从此文件夹中读取所有图像。
绘画程序
由于步进电机没有内部定位反馈系统,它们的位置必须由软件跟踪。我编写的程序使用网格系统跟踪 LED 的位置,以便轻松缩放。当 Arduino Mega 启动时,步进器位置设置为 0、0,然后找到并读取第一个图像。然后,LED 闪烁五次,让摄影师知道几乎可以开始拍摄了。位图的读取方式是首先循环遍历每一行,然后在每一行中读取每一列。通过了解当前的行和列,步进电机可以移动到相同的位置。在每个位置,LED 会更改为相应像素的颜色。
(re)-创建图像
插入SD卡并为电机插入12v电源后,就可以打开机器了。在我的相机上,我将其设置为 20 秒的曝光时间、F36 的光圈、100 的 ISO 和 -5 档的曝光补偿,以最大限度地减少重影效果。绘制的第一张图片是一个精灵球,见这里:
虽然有点模糊,但还是可以清楚地看到形状。然后它创建了一个心脏位图:
由于此图像只有 9 x 9 像素,因此每个单独的像素都不太明确。最后,我画了一张马里奥跳跃的图:
这张图片重影重,主要是因为色彩鲜艳的像素很多。
未来的改进思路
我创作的光绘效果比我最初想象的要好得多,但仍有改进的空间。我想做的主要事情是通过让 LED 在变暗时移动,然后仅在静止时点亮来减少模糊量。这种技术将大大提高重建图像的清晰度。
代码
- 光绘计划
光绘程序C/C++
//部分来自Adafruit的位图读取功能#include#include #include "DRV8825.h"#define MOTOR_STEPS 200#define RPM 150#define MICROSTEPS 4//引脚定义#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 或-FLAG_1 翻转方向 //1 或-FLAG_1 翻转方向1 或 -1 翻转方向#define STEPS_PER_MM (3.75 * MICROSTEPS) //移动 1mm 所需的步数#define SPACE_BETWEEN_POSITIONS 5 //每次移动 5mm#define R A0#define G A1#define B A2#define SD_CS 22int 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);120 setup(10begin);120 init_steppers(); SD.begin(SD_CS); createBitmaps(); stepperX.disable(); stepperY.disable(); while(1);}void loop() {}void createBitmaps(){ File dir =SD.open("bitmaps"); while(true){ 文件位图 =dir.openNextFile();如果(!位图){ 中断;油漆位图(位图);延迟(15000); } }#define BUFFPIXEL 20void paintBitmap(File bmpFile){ int bmpWidth, bmpHeight; uint8_t bmpDepth; uint32_t bmpImageOffset; uint32_t 行大小; // 并非总是 =bmpWidth;可能有填充 uint8_t sdbuffer[3 * BUFFPIXEL]; // 像素缓冲区(每像素 R+G+B) uint8_t buffidx =sizeof(sdbuffer); // 当前位置在 sdbuffer boolean goodBmp =false; // 在有效的标头解析上设置为 true boolean flip =true; // BMP 自下而上存储 int w, h, row, col; uint8_t r, g, b; uint32_t pos =0,startTime =毫秒(); Serial.println(); Serial.print("加载图像'"); Serial.print(bmpFile.name()); Serial.println('\''); // 打开 SD 卡上请求的文件 // 解析 BMP 头 if (read16(bmpFile) ==0x4D42) { // BMP 签名 Serial.print("File size:"); Serial.println(read32(bmpFile)); (无效)read32(bmpFile); // 读取并忽略创建者字节 bmpImageOffset =read32(bmpFile); // 图像数据开始 Serial.print("Image Offset:"); Serial.println(bmpImageOffset, DEC); // 读取 DIB 头 Serial.print("Header size:"); Serial.println(read32(bmpFile)); bmpWidth =read32(bmpFile); bmpHeight =read32(bmpFile); if (read16(bmpFile) ==1) { // # 平面 -- 必须是 '1' bmpDepth =read16(bmpFile); // 每像素位数 Serial.print("Bit Depth:"); Serial.println(bmpDepth); if ((bmpDepth ==24) &&(read32(bmpFile) ==0)) { // 0 =未压缩的 goodBmp =true; // 支持的 BMP 格式——继续! Serial.print("图片尺寸:"); Serial.print(bmpWidth); Serial.print('x'); Serial.println(bmpHeight); // BMP 行被填充(如果需要)到 4 字节边界 rowSize =(bmpWidth * 3 + 3) &~3; // 如果 bmpHeight 为负,则图像为自上而下的顺序。 // 这不是经典,而是在野外观察到的。如果(bmpHeight <0){ bmpHeight =-bmpHeight;翻转 =假; } // 要加载的裁剪区域 w =bmpWidth; h =bmpHeight; if(bmpWidth*bmpHeight>290){ //Too large Serial.println("文件太大无法打印。");返回; } for(uint8_t i=0; i<5;i++){analogWrite(R, 150);延迟(500);模拟写入(R,0);延迟(500); } for (row =0; row =sizeof(sdbuffer)) { // 实际上 bmpFile.read(sdbuffer, sizeof(sdbuffer)); buffidx =0; // 将索引设置为开始 } // 将像素从 BMP 格式转换为 TFT 格式,推送显示 b =sdbuffer[buffidx++]; g =sdbuffer[buffidx++]; r =sdbuffer[buffidx++]; moveToPosition(col, row);激活LED(r,g,b); // 优化! //tft.pushColor(tft.Color565(r,g,b)); } // 结束像素analogWrite(R, 0);模拟写入(G,0);模拟写入(B,0); } // 结束扫描线 Serial.print("Loaded in "); Serial.print(millis() - startTime); Serial.println("毫秒"); } // 结束goodBmp } } bmpFile.close(); moveToPosition(0,0); if (!goodBmp) Serial.println("BMP 格式无法识别。");}uint16_t read16(File f) { uint16_t result; ((uint8_t *)&result)[0] =f.read(); // LSB ((uint8_t *)&result)[1] =f.read(); // MSB 返回结果;}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 返回结果;}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);模拟写入(R,R);模拟写入(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);当前位置[X] =x;当前位置[Y] =y; Serial.print("步进位置:"); 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);}
定制零件和外壳
示意图
制造工艺