The camera alone can do plenty, but with the data it captures you can do even more. One way to explore your thermal images is to use sketches in a programming language from Processing.org that runs on a full-blown computer. There is an Adafruit tutorial on installing the Processing environment on a Raspberry Pi, and their website has links for Mac and Windows users as well.
The Inspector Sketch
Once you have the Processing package installed and set up, here is a sketch to download and try. Open a new sketch window, copy the following code and paste it there, and save it as ConvertBMPinspector01. Be sure you're working in the Processing environment and not in the Arduino environment. It's an easy mistake to make.
// ConvertBMPinspector01 - Read and enlarge a modified 32x24 24-bit gray BMP file, // display an upscaled 256x192 BMP image in 256 colors. // Ver. 1 - Fetch filenames and display BMPs in sequence. // Add nav buttons and mouseover pixel temperatures // This sketch does no checking for file compatibility. // Only frm_____.bmp images from the thermal camera sketch will work. // Any other files in the data folder will fail. import java.util.Date; byte b[], colorPal[]; // Buffers for input file bytes and for colors int i, fileCount = 0, BGcolor = 48, colorMap = 1, butnsX = 30, butnsY = 290, offsetX = 153, offsetY = 6, // These value pairs control where the onscreen features appear numbersX = 40, numbersY = 48, probeX = 190, probeY = 210; boolean celsiusFlag = false; float fixedPoint[]; String[] filenames; void setup() { size(480, 360); // Size must be the first statement background(BGcolor); // Clear the screen with a gray background colorPal = new byte[1024]; // Prepare a 1K color table loadColorTable(colorMap, 0); // Load color table, 1 == ironbow palette fixedPoint = new float[5]; // A buffer for appended fixed point values String path = sketchPath() + "/data"; // Read from the "/data" subdirectory filenames = listFileNames(path); fileCount = filenames.length; i = 0; if(fileCount < 1) { println("No files found. Stopping."); noLoop(); } else { loadBMPscreen(i); // Read in the first frame for inspection } } void draw() { int sampleX, sampleY, pixelVal; float sampleTemp; sampleX = (mouseX - offsetX) >> 3; // Map mouse position to BMP pixel space sampleY = 23 - ((mouseY - offsetY) >> 3); noStroke(); smooth(); fill(BGcolor + 16); rect(probeX, probeY, 180, 40); // Clear the interactive window space if((sampleX >= 0) && (sampleX < 32) && (sampleY >= 0) && (sampleY < 24)) { // Mouse within BMP image bounds? pixelVal = b[54 + (32 * sampleY + sampleX) * 3] & 0xff; // Read the 8-bit pixel value fill(colorPal[4 * pixelVal + 2] & 0xFF, colorPal[4 * pixelVal + 1] & 0xFF, colorPal[4 * pixelVal + 0] & 0xFF); rect(probeX, probeY, 180, 40); fill(BGcolor); rect(probeX + 10, probeY + 10, 160, 20); // Draw a colorized frame for the interactive temp readout sampleTemp = (float(pixelVal) + 1.0) / 257.0 * (fixedPoint[3] - fixedPoint[1]) + fixedPoint[1]; if(!celsiusFlag) sampleTemp = sampleTemp * 1.8 + 32.0; fill(255); // Ready to display white interactive text textSize(11); text(sampleX, probeX + 154, probeY + 19); // Display X Y position text(sampleY, probeX + 154, probeY + 29); textSize(15); text(sampleTemp, probeX + 60, probeY + 25); // Display temperature if(pixelVal == 0 && fixedPoint[0] < fixedPoint[1]) // Pixel values clipped at bottom limit? text("<", probeX + 40, probeY + 25); // Show out-of-range indicator if(pixelVal == 255 && fixedPoint[4] > fixedPoint[3]) // Clipped at top? text(">", probeX + 40, probeY + 25); // Same } noSmooth(); // Clear any highlighted buttons stroke(0); noFill(); for(sampleX = 0; sampleX < 8; ++sampleX) rect(butnsX + sampleX * 52, butnsY, 51, 24); sampleX = mouseX - butnsX; sampleY = mouseY - butnsY; if(sampleX >=0 && sampleX < 416 && sampleY >= 0 && sampleY < 24) { // Mouse over buttons? sampleX = sampleX / 52; // Map mouse X to button X space stroke(BGcolor + 64); rect(butnsX + sampleX * 52, butnsY, 51, 24); // Highlight border around a button } } void keyPressed() { // Load a different thermal BMP image based on keystroke switch(key) { case '.': // Next image i = (i + 1) % fileCount; break; case ',': // Prev Image i = (i + fileCount - 1) % fileCount; break; case '>': // 16 images forward i = i + 16 < fileCount ? i + 16 : fileCount - 1; break; case '<': // 16 images back i = i - 16 < 0 ? 0 : i - 16; break; case '/': // Last image i = fileCount - 1; break; case 'm': // First image i = 0; break; } loadBMPscreen(i); } void mousePressed() { int sampleX, sampleY; sampleX = mouseX - butnsX; sampleY = mouseY - butnsY; if(sampleX >=0 && sampleX < 416 && sampleY >= 0 && sampleY < 24) { // Is mouse over button row? sampleX = sampleX / 52; // Map mouse X to button X space switch(sampleX) { case 1: // First image i = 0; break; case 2: // 16 images back i = i - 16 < 0 ? 0 : i - 16; break; case 3: // Prev Image i = (i + fileCount - 1) % fileCount; break; case 4: // Next image i = (i + 1) % fileCount; break; case 5: // 16 images forward i = i + 16 < fileCount ? i + 16 : fileCount - 1; break; case 6: // Last image i = fileCount - 1; break; case 7: // Change color map loadColorTable(colorMap = (colorMap + 1) % 5, 0); // Load color table break; default: // Toggle C/F celsiusFlag = !celsiusFlag; break; } loadBMPscreen(i); } } void loadBMPscreen(int fileIndex) { int x, y; b = loadBytes(filenames[fileIndex]); // Open a file and read its 8-bit data background(BGcolor); // Clear screen enlarge8bitColor(); // Place colored enlarged image on screen for(x = 0; x < 5; ++x) { // Rebuild 5 float values from next 4*n bytes in the file fixedPoint[x] = expandFloat(b[2360 + (x * 4) + 0], b[2360 + (x * 4) + 1], b[2360 + (x * 4) + 2], b[2360 + (x * 4) + 3]); } y = ((b[2387] & 0xff) << 24) + ((b[2386] & 0xff) << 16) + ((b[2385] & 0xff) << 8) + (b[2384] & 0xff); // Reassemble a milliseconds time stamp textSize(10); // Print text labels for the frame stats smooth(); fill(255); text(filenames[fileIndex], numbersX + 5, numbersY + 40); // Show current filename if(celsiusFlag) text("Frame\n\n\nSeconds\n\nDegrees C", numbersX + 5, numbersY + 8); else text("Frame\n\n\nSeconds\n\nDegrees F", numbersX + 5, numbersY + 8); text("Approximate temperatures based on 8-bit pixel values", probeX - 42, probeY + 52); // Show approximation disclaimer textSize(15); text(fileIndex, numbersX + 5, numbersY + 25); // Print frame number text(float(y) * 0.001, numbersX, numbersY + 74); // Print time stamp in seconds if(celsiusFlag) { // Show 3 temps in Celsius fill(255, 128, 64); text(fixedPoint[4], numbersX, numbersY + 108); fill(255, 200, 64); text(fixedPoint[2], numbersX, numbersY + 128); fill(128, 128, 255); text(fixedPoint[0], numbersX, numbersY + 148); } else { // or show them in Farenheit fill(255, 128, 64); text(fixedPoint[4] * 1.8 + 32.0, numbersX, numbersY + 108); fill(255, 200, 64); text(fixedPoint[2] * 1.8 + 32.0, numbersX, numbersY + 128); fill(128, 128, 255); text(fixedPoint[0] * 1.8 + 32.0, numbersX, numbersY + 148); } noSmooth(); stroke(0); fill(BGcolor + 24); for(x = 0; x < 8; ++x) // Draw 8 button rectangles rect(butnsX + x * 52, butnsY, 51, 24); for(x = 0; x < 50; ++x) { // Paint a mini colormap gradient within last button y = int(map(x, 0, 50, 0, 255)); stroke(colorPal[4 * y + 2] & 0xFF, colorPal[4 * y + 1] & 0xFF, colorPal[4 * y + 0] & 0xFF); line(butnsX + 365 + x, butnsY + 1, butnsX + 365 + x, butnsY + 23); } smooth(); // Add text labels to buttons fill(255); textSize(15); text("|< << < > >> >|", butnsX + 70, butnsY + 17); if(celsiusFlag) text("C", butnsX + 20, butnsY + 18); else text("F", butnsX + 20, butnsY + 18); } void enlarge8bitColor() { // Convert a small gray BMP array and plot an enlarged colormapped version int x, y; noStroke(); for(y = 0; y < 24; ++y) { // Count all source pixels for(x = 0; x < 32; ++x) { int pixMid = b[54 + ((32 * y + x) + 0) * 3] & 0xFF; fill(colorPal[4 * pixMid + 2] & 0xFF, colorPal[4 * pixMid + 1] & 0xFF, colorPal[4 * pixMid + 0] & 0xFF); // Get color from table rect(offsetX + 8 * x, offsetY + 8 * (23 - y), 8, 8); // Draw a square pixel, bottom up } } } void loadColorTable(int choiceNum, int offset) { int i, x; switch(choiceNum) { case 1: // Load 8-bit BMP color table with computed ironbow curves for(x = 0; x < 256; ++x) { float fleX = (float)x / 255.0; float fleG = 255.9 * (1.02 - (fleX - 0.72) * (fleX - 0.72) * 1.96); fleG = (fleG > 255.0) || (fleX > 0.75) ? 255.0 : fleG; // Truncate curve i = (int)fleG; colorPal[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals fleG = fleX * fleX * 255.9; i = (int)fleG; colorPal[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals fleG = 255.9 * (14.0 * (fleX * fleX * fleX) - 20.0 * (fleX * fleX) + 7.0 * fleX); fleG = fleG < 0.0 ? 0.0 : fleG; // Truncate curve i = (int)fleG; colorPal[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals } break; case 2: // Compute quadratic "firebow" palette for(x = 0; x < 256; ++x) { float fleX = (float)x / 255.0; float fleG = 255.9 * (1.00 - (fleX - 1.0) * (fleX - 1.0)); i = (int)fleG; colorPal[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals fleG = fleX < 0.25 ? 0.0 : (fleX - 0.25) * 1.3333 * 255.9; i = (int)fleG; colorPal[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals fleG = fleX < 0.5 ? 0.0 : (fleX - 0.5) * (fleX - 0.5) * 1023.9; i = (int)fleG; colorPal[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals } break; case 3: // Compute "alarm" palette for(x = 0; x < 256; ++x) { float fleX = (float)x / 255.0; float fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 1.0); i = (int)fleG; colorPal[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : (fleX - 0.875) * 8.0); i = (int)fleG; colorPal[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 0.0); i = (int)fleG; colorPal[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals } break; case 4: // Grayscale, black hot for(x = 0; x < 256; ++x) { colorPal[offset + x * 4 + 2] = byte(255 - x & 0xFF); // Red vals colorPal[offset + x * 4 + 1] = byte(255 - x & 0xFF); // Grn vals colorPal[offset + x * 4 + 0] = byte(255 - x & 0xFF); // Blu vals } break; default: // Grayscale, white hot for(x = 0; x < 256; ++x) { colorPal[offset + x * 4 + 2] = byte(x & 0xFF); // Red vals colorPal[offset + x * 4 + 1] = byte(x & 0xFF); // Grn vals colorPal[offset + x * 4 + 0] = byte(x & 0xFF); // Blu vals } } } // Rebuild a float from a fixed point decimal value encoded in 4 bytes float expandFloat(byte m1, byte m2, byte e1, byte e2) { int fracPart; float floatPart; fracPart = ((e2 & 0xff) << 8) + (e1 & 0xff); // Reassemble 16-bit value floatPart = (float)fracPart / 49152.0; // Convert into fractional portion of float fracPart = ((m2 & 0xff) << 8) + (m1 & 0xff); // Reassemble 16-bit value return ((float)fracPart + floatPart) - 1000.0; // Complete reconstructing original float } String[] listFileNames(String dir) { // Return the filenames from a directory as an array of Strings File file = new File(dir); if (file.isDirectory()) { String names[] = file.list(); return names; } else // It's not a directory return null; }
Once you've saved this sketch in the Sketchbook folder with a filename, Processing has placed it in a subfolder with the same name. It's good to know where to find this subfolder when importing thermal image datasets. The editor window can quickly open it for you by starting at the menu bar and selecting Sketch>Show sketch folder, where the new file should appear.
So far, so good. There are two more sketches to go.
Page last edited January 20, 2025
Text editor powered by tinymce.