Ever wanted to use your thermal camera with Arduino but found it difficult to go beyond the tutorials code? Let's see the easiest possible way to view your thermal camera streaming without an LCD display!

Arduino thermal image rgb vs ascii

MLX90640 thermal camera

For Arduino there are essentially two thermal camera available: the AMG8833 and the MLX90640.

The AMG8833 is 8x8 and the MLX90640 is 32x24.

They're not cheap, it is true.

But if you have to spend money, I strongly advise you to buy the MLX90640: I have one and it's not that accurate. I can't imagine how low definition would be the AMG8833.

If you want to actually get something meaningful from the camera, the AMG8833 won't give you any good results.

Sure, you can do interpolation: interpolation would give you the impression you have a better definition, but you're just "inventing" values you don't actually have.

For demo projects it could be enough. But for any serious application, spend 20$ more and buy an MLX90640.

MLX90640 eloquent library

As you may know if you read my previous posts, I strongly believe in "eloquent" code, that is code that's as easy as possible to read.

How many lines do you think you need to read a MLX90640 camera? Well, not that much in fact.

#include "EloquentMLX90640.h"

using namespace Eloquent::Sensors;

float buffer[768];
MLX90640 camera;

void setup() {
  Serial.begin(115200);

  if (!camera.begin()) {
    Serial.println("Init error");
    delay(50000);
  }
}

void loop() {
  camera.read(buffer);
  delay(3000);
}

If you skip the declaration lines, you only need a begin() and read() call.

That's it.

What begin() does is to run all of the boilerplate code I mentioned earlier (checking the connection and initializing the parameters).

read() populates the buffer you pass as argument with the temperature readings.

From now on, you're free to handle that array as you may like: this is the most flexible way for the library to handle any use-case. It simply does not pose any restriction.

You can find the camera code at the end of the page or on Github.

Printing as ASCII Art

Now that you have this data, you may want to actually "view" it. Well, that's not an easy task as one may hope.

You will need an LCD if you want to create a standalone product. If you have one, it'll be the best, it's a really cute project to build.

Here's a video from Adafruit that showcases even a 3D-printed case.

If you don't have an LCD, though, it is less practical to access your image.

I did this in the past, and it meant creating a Python script reading the serial port every second and updating a plot.
It works, sure, but it's not the most convenient way to handle it.

This is the reason I thought about ASCII art: it is used to draw images in plain text, so you can view them directly in the serial monitor.

Of course they will not be as accurate or representative as RGB images, but can give you an idea of what you're framing in realtime.

I wrote a class to do this. Once imported in your sketch, it is super easy to get it working.

#include "EloquentAsciiArt.h"

using namespace Eloquent::ImageProcessing;

float buffer[768];
uint8_t bufferBytes[768];
MLX90640 camera;
// we need to specify width and height of the image
AsciiArt<32, 24> art(bufferBytes);

void loop() {
  camera.read(buffer);

  // convert float image to uint8
  for (size_t i = 0; i < 768; i++) {
    // assumes readings are in the range 0-40 degrees
    // change as per your need
    bufferBytes[i] = map(buffer[i], 0, 40, 0, 255);
  }

  // print to Serial with a border of 2 characters, to distinguish one image from the next
  art.print(&Serial, 2);
  delay(2000);
}

As you can see, you need to create an AsciiArt object, map the image pixels in the range 0-255 and call the print() method: easy peasy!

You can find the ASCII art generator code at the end of the page or on Github.

Here's the result of the sketch. It's a video of me putting my arms at the top of my head, once at a time, then standing up.

Resize the Serial Monitor as only a single frame at a time is visble to have a "video streaming" effect

Of course the visual effect won't be as impressive as an RGB image, but you can clearly see my figure moving.

The real bad part is the "glitch" you see between each frame when the scrolling happens: this is something I don't know if it's possible to mitigate.


Check the full project code on Github


#pragma once

#include "Wire.h"
#include "MLX90640_API.h"
#include "MLX90640_I2C_Driver.h"

#ifndef TA_SHIFT
//Default shift for MLX90640 in open air
#define TA_SHIFT 8
#endif

namespace Eloquent {
    namespace Sensors {

        enum class MLX90640Status {
            OK,
            NOT_CONNECTED,
            DUMP_ERROR,
            PARAMETER_ERROR,
            FRAME_ERROR
        };

        class MLX90640 {
        public:
            /**
             *
             * @param address
             */
            MLX90640(uint8_t address = 0x33) :
                _address(address),
                _status(MLX90640Status::OK) {

            }

            /**
             *
             * @return
             */
            bool begin() {
                Wire.begin();
                Wire.setClock(400000);

                return isConnected() && loadParams();
            }

            /**
             *
             * @return
             */
            bool read(float result[768]) {
                for (byte x = 0 ; x < 2 ; x++) {
                    uint16_t frame[834];
                    int status = MLX90640_GetFrameData(_address, frame);

                    if (status < 0)
                        return fail(MLX90640Status::FRAME_ERROR);

                    float vdd = MLX90640_GetVdd(frame, &_params);
                    float Ta = MLX90640_GetTa(frame, &_params);
                    float tr = Ta - TA_SHIFT;
                    float emissivity = 0.95;

                    MLX90640_CalculateTo(frame, &_params, emissivity, tr, result);
                }
            }

        protected:
            uint8_t _address;
            paramsMLX90640 _params;
            MLX90640Status _status;

            /**
             * Test if device is connected
             * @return
             */
            bool isConnected() {
                Wire.beginTransmission(_address);

                if (Wire.endTransmission() == 0) {
                    return true;
                }

                return fail(MLX90640Status::NOT_CONNECTED);
            }

            /**
             *
             * @return
             */
            bool loadParams() {
                uint16_t ee[832];
                int status = MLX90640_DumpEE(_address, ee);

                if (status != 0)
                    return fail(MLX90640Status::DUMP_ERROR);

                status = MLX90640_ExtractParameters(ee, &_params);

                if (status != 0)
                    return fail(MLX90640Status::PARAMETER_ERROR);

                return true;
            }

            /**
             * Mark a failure
             * @param status
             * @return
             */
            bool fail(MLX90640Status status) {
                _status = status;

                return false;
            }
        };
    }
}
#pragma once

#include "Stream.h"

namespace Eloquent {
    namespace ImageProcessing {

        /**
         *
         * @tparam width
         * @tparam height
         */
        template<size_t width, size_t height>
        class AsciiArt {
        public:
            AsciiArt(const uint8_t *data) {
                _data = data;
            }

            /**
             * Get pixel at given coordinates
             * @param x
             * @param y
             * @return
             */
            uint8_t at(size_t x, size_t y) {
                return _data[y * width + x];
            }

            /**
             * Print as ASCII art picture
             * @param stream
             */
            void print(Stream *stream, uint8_t frameSize = 0) {
                const char glyphs[] = " .,:;xyYX";
                const uint8_t glyphsCount = 9;

                printAsciiArtHorizontalFrame(stream, frameSize);

                for (size_t y = 0; y < height; y++) {
                    // vertical frame
                    for (uint8_t k = 0; k < frameSize; k++)
                        Serial.print('|');

                    for (size_t x = 0; x < width; x++) {
                        const uint8_t glyph = floor(((uint16_t) at(x, y)) * glyphsCount / 256);

                        stream->print(glyphs[glyph]);
                    }

                    // vertical frame
                    for (uint8_t k = 0; k < frameSize; k++)
                        Serial.print('|');

                    stream->print('\n');
                }

                printAsciiArtHorizontalFrame(stream, frameSize);
                stream->flush();
            }

        protected:
            const uint8_t *_data;

            /**
             *
             * @param stream
             * @param frameSize
             */
            void printAsciiArtHorizontalFrame(Stream *stream, uint8_t frameSize) {
                for (uint8_t i = 0; i < frameSize; i++) {
                    for (size_t j = 0; j < width + 2 * frameSize; j++)
                        stream->print('-');
                    stream->print('\n');
                }
            }
        };
    }
}
Help the blow grow