Skip to main content

UTi712S

Extract the thermal matrix from BMP files saved by the UNI-T UTi712S, convert it to degrees Celsius with the same scale the camera software uses, and look up the temperature of each point in a viewer.

The file does not contain the sensor reading. It contains the 8-bit screen image and three spot temperatures (minimum, maximum, and center). The Celsius matrix is the interpolation ImageFile.dll builds from those values. It reproduces what the official program shows. It does not improve the measurement.

The camera

The UTi712S stores captures as BMP files. The sensor is 120 x 90. The screen is 320 x 240, and the file comes out at 240 x 320, the same frame turned upright. The range published by UNI-T is -20 °C to 400 °C, with an accuracy of ±2 °C or ±2 %, whichever is greater. The factory emissivity is 0.95.

Those three figures (range, accuracy, emissivity) describe the spots the camera measures. They do not describe the byte of each pixel.

Installation

Python 3.10 or newer is required. The package is on PyPI. With pipx:

pipx install uti712s

That installs two commands: uti712s-extract and uti712s-view.

Pillow is optional. Without it the tool writes the JSON and the CSV files, and skips the PNG files. The extra is named png. Inside the pipx environment:

pipx inject uti712s pillow

The viewer uses tkinter, which is not installed by pip. On Debian or Ubuntu: sudo apt install python3-tk. It has to be available to the same Python that pipx uses.

From the repository, without going through PyPI:

pipx install git+https://github.com/pablogventura/uti712s.git

Or, from a local copy:

pipx install .

Usage

uti712s-extract 'images/IMG 0001.bmp'

Without installing, from the repository:

python3 uti712s_extract.py 'images/IMG 0001.bmp'

The files are written next to the BMP:

File Contents
IMG 0001_metadata.json Spot temperatures, anchors, mode, and control pixels
IMG 0001_intensities.csv 8-bit matrix, one image row per line, no header
IMG 0001_celsius.csv The same grid in °C, eight decimal places
IMG 0001_intensities.png The matrix in gray, without the camera interface
IMG 0001_thermal_no_overlay.png The same matrix colored with the embedded palette

Row 0 is the top edge. In mode 7, which is the mode of the captures in images/, the order is the file order.

The metadata JSON uses these fields:

Field Meaning
file BMP file name
exported_resolution [width, height]
file_unit Unit stored in the file (°C or °F)
orientation_mode Orientation byte
maximum_temperature, minimum_temperature, center_temperature Footer spots, in the file unit, already divided by 10
emissivity Footer byte divided by 100
cold_anchor_intensity, hot_anchor_intensity Intensities tied to the minimum and the maximum
minimum_point_xy, maximum_point_xy Footer coordinates after the orientation transform
center_point_xy Center the software draws
curve_pixel_xy Pixel that anchors the two-segment scale
curve_intensity, cross_intensity Intensity at the curve pixel and at the drawn center
curve_celsius, cross_celsius Those two pixels in °C
minimum_intensity, maximum_intensity Lowest and highest byte in the matrix
warning What the Celsius matrix is, and what it is not
exports Paths written next to the BMP

From Python

read_capture reads the BMP and does not write files. celsius_at computes one point with the two-segment scale. extract writes the JSON, the CSV files, and, when Pillow is installed, the PNG files.

from pathlib import Path
from uti712s_extract import read_capture, extract

capture = read_capture(Path("images/IMG 0001.bmp"))
print(capture.celsius_at(119, 159))
extract(Path("images/IMG 0001.bmp"))

Viewer

Opens the thermal scene without the camera interface and shows the estimated temperature of each pixel. It uses the embedded palette and the same scale as the CSV. Opening a BMP does not write files.

uti712s-view
uti712s-view 'images/IMG 0001.bmp'

Without installing, from the repository: python3 uti712s_view.py and python3 uti712s_view.py 'images/IMG 0001.bmp'.

File, Open loads another BMP. The mouse wheel over the image changes the zoom from 1 to 6. Each source pixel stays a solid block. The wheel over the panel scrolls the text when it does not fit in the window.

The panel has three blocks.

Under the pointer. The temperature in °C, to one decimal place, of the pixel under the pointer. It also shows the column and the row. Columns count from the left and rows from the top. Brightness is the byte from 0 to 255 stored in the file. The temperature is the Celsius estimate. When the pointer is off the image, the block asks you to move it onto the image.

Pinned point. A click keeps that reading in place while the pointer moves. Another click, or Escape, releases it. The point is drawn with a pink square, slightly larger than the pixel.

Camera readings. The three footer values, the emissivity, and the mode.

  • Minimum and maximum: the coldest and hottest spots the camera measured. They are not drawn on the image. Those coordinates sometimes fall outside the frame, and they do not always match the coldest or hottest byte.
  • Center: the center reading. The cyan square lands on that value. Every other pixel is estimated from these three.
  • Emissivity: the value set on the camera. In these captures it is 0.95, the factory setting.
  • Mode 7: the photo is not rotated. The top stays the top.

There are two fixed marks. The cyan square is the curve pixel. In these captures it is column 119, row 159, and its temperature is the center reading. The yellow cross is the center drawn by the software, at column 120, row 160. In mode 7 they are different pixels. When the pointer lands on one of them, the panel says which one.

What is in the BMP

The 32 captures in images/ are 230995 bytes and share this shape: a 16-bit BMP, 240 x 320, uncompressed, plus an extra block of 77341 bytes.

The official program does not read the width or height from the header. It fixes 240 x 320 and starts the thermal area at offset 0x25839 (153657). This script reads the size from the header and enters at bfSize + 3. In a 16-bit 240 x 320 BMP, bfSize is 153654 (54 header bytes plus 153600 pixel bytes), so bfSize + 3 lands on the same byte. It also requires the extra block to be 3 + width*height + 512 + 26 bytes and to start with three zeros.

0            BMP header and 16-bit pixels            153654 bytes
153654       00 00 00                                3 bytes
153657       intensities, 1 byte per pixel           240 * 320 = 76800
230457       RGB565 palette, 256 colors              512
230969       footer                                  26
230995       end

The visible BMP is the screen: crosshair, labels, battery, date, and color bar. The 8-bit matrix is the thermal scene without that interface. The vertical origin matches the photo: the top is the top.

The embedded palette does not reproduce the BMP pixels. On these captures the exact match is effectively none. The screen draws the interface on top and uses a different color stretch. The PNG without the overlay shows the clean scene, with the file palette, not a copy of the screen.

Palette

256 colors, 16 bits, little-endian, RGB565:

Channel Bits Expansion used by the software
Red 15..11 the 5 bits, shifted left by 3 (maximum 248)
Green 10..5 the 6 bits, shifted left by 2
Blue 4..0 the 5 bits, shifted left by 3

The index is the raw intensity byte, not stretched to the scene range.

Little-endian. Temperatures are signed integers, in tenths. Coordinates are unsigned integers.

Offset Type Field
0 uint8 Unit. 0 is °C, 1 is °F
1 int16 Maximum temperature x 10
3 int16 Minimum temperature x 10
5 uint8 Hot anchor: intensity the software treats as the maximum
6 uint8 Cold anchor: intensity the software treats as the minimum
7 int16 Center temperature x 10
9 uint8 Emissivity x 100
10 uint8 Orientation mode
11 3 bytes Unread by the software. In these captures they are 0
14 uint16 Minimum X
16 uint16 Minimum Y
18 uint16 Maximum X
20 uint16 Maximum Y
22 uint16 Stored center X
24 uint16 Stored center Y

Across the 32 captures: unit °C, emissivity 0.95, mode 7, cold anchor 0, hot anchor equal to the maximum of the matrix, stored center at (120, 160).

Bytes 5 and 6 together look like an int16. They are separate fields. The software reads them one byte at a time. Because byte 6 is 0, that int16 matches the maximum intensity, which is why an earlier parse treated it as "the matrix maximum" packed into a single field.

Orientation mode

The mode is byte 10. It changes the matrix order before conversion, and also how the minimum and maximum coordinates are interpreted. The drawn center does not come from the footer: the software fixes it from the mode.

Mode Matrix Minimum and maximum Drawn center
1 output[row, col] = source[height * (width - col - 1) + row] x' = width - y - 1, y' = x - 1 (height/2 - 1, width/2 - 1) = (159, 119)
2 and 5 the buffer reversed x' = width - x - 1, y' = height - y - 1 (width/2 - 1, height/2 - 1) = (119, 159)
any other, including 7 left as stored left as stored (width/2, height/2) = (120, 160)

All 32 captures are mode 7. Modes 1, 2, and 5 are implemented as in the DLL, but this set does not contain them.

Minimum and maximum coordinates can fall outside the raster. These photos include x = 240 or y = 321. The software stores them anyway, without clipping. Sometimes the marker is a few pixels from the intensity extreme, and sometimes tens of pixels away. The spot temperature and the coldest or hottest byte are different data: the byte has already been through contrast enhancement.

Celsius scale

The curve pixel is separate from the crosshair. After reordering, the software takes intensity Ic at (width/2 - 1, height/2 - 1). At 240 x 320 that is (119, 159), one pixel up and to the left of the crosshair (120, 160).

With temperatures still in tenths:

  • if intensity p is greater than or equal to Ic, the line joins (hot anchor, maximum) to (Ic, center);
  • if it is lower, the line joins (Ic, center) to (cold anchor, minimum).
raw = p * (Tb - Ta) / (Ib - Ia) + (Ta * Ib - Tb * Ia) / (Ib - Ia)
°C  = raw / 10

(Ia, Ta) is the first point of the segment and (Ib, Tb) the second. When the file is in Fahrenheit, this script converts the result to Celsius with (°F - 32) / 1.8. The official program, in that case, keeps the first matrix in the file unit and builds another with value * 1.8 + 32. The captures in images/ are in °C, so the CSV matches the program's matrix.

By construction, the curve pixel equals the center temperature. The crosshair does not. In IMG 0001.bmp:

Intensity °C
Curve (119, 159) 17 13.3
Crosshair (120, 160) 16 12.84705882
Cold anchor 0 5.6
Coldest pixel in that photo 1 6.05294118
Hot anchor 253 21.6

The footer minimum is 5.6 °C and the maximum is 21.6 °C. The coldest pixel does not reach 5.6 °C because the cold anchor is intensity 0 and that pixel is 1.

What cannot be recovered

The sensor delivers much more than 8 bits. Before saving, the camera enhances contrast and stretches the result to 240 x 320. Many different temperatures land on the same byte, and the table for that enhancement is not in the file. Three measured points can pin the curve to the minimum, the maximum, and the center. Any other pixel remains an estimate, and another curve through the same three points is not a better measurement.

The error follows the span of the scene. With less than 2 °C of difference, as in IMG 0008.bmp or IMG 0031.bmp, it stays under 0.1 °C. In IMG 0001.bmp the crosshair already differs by 0.45 °C from the center measurement. In a scene of hundreds of degrees the difference is tens of degrees.

IMG 0003.bmp stores a maximum of 431.7 °C and the screen shows OL. IMG 0004.bmp stores 429.6 °C, above the -20 °C to 400 °C range published for this camera. Those maxima are in the file, and the camera does not treat them as a normal reading.

For a point that matters, the reading to trust is the spot tool on the camera.

Official software and other parsers

UNI-T publishes the UTi712S PC Software (June 2022, a Windows Setup.exe inside the zip). The logic for this format is in ImageFile.dll, in refreshParam and in the intensity-to-temperature step. This repository does not include that installer.

The official program uses this same two-segment scale. It does not hide a radiometric plane.

UNI-T Thermal Utilities documents a similar BMP, the one from the UTi260B. It does not apply to these files: that camera stores 24-bit BMPs, without the 3 pad bytes, and lists the maximum point first. The author describes the same limit in temperature_issue.md: the embedded image is already contrast-enhanced, and after the fact only the minimum, the maximum, and the center can be trusted.

Samples

images/ contains 32 BMPs taken with a UTi712S, from September 2020 to August 2026. All of them match the format above, in mode 7 and in °C. They are there to check the parser and the scale. They are not a calibration.

Metadata

Release files for uti712s 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for uti712s 0.2.0
File Size Uploaded Provenance Actions
uti712s-0.2.0.tar.gz 15.1 kB Without provenance Details for uti712s-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for uti712s 0.2.0
File Interpreter ABI Platform Actions
uti712s-0.2.0-py3-none-any.whl Python 3 none any Details for uti712s-0.2.0-py3-none-any.whl

Total release size: 30.7 kB

Release files / uti712s-0.2.0.tar.gz

Download URL uti712s-0.2.0.tar.gz
Size 15.1 kB
Tags Source
SHA-256 checksum
How to use checksums
583a9fc934944a21b21d951f953f16b7dd709e33cb35a527993174335092da7c
BLAKE2b-256 checksum
How to use checksums
b55239ed326cf3cc32649ab928b0b892ac463641915293756988760f8dd9fc14
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release files / uti712s-0.2.0-py3-none-any.whl

Download URL uti712s-0.2.0-py3-none-any.whl
Size 15.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
551441d1e596de2a0e7b61a3ecc7af673e11c7272c39c577748ace33a01779a1
BLAKE2b-256 checksum
How to use checksums
5e2817010a248821175ec7ab733c4fb2a124a7c3ef3400373d9b5b247d85f00f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Security for uti712s 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page