This project demonstrates an approach to improve object detection—specifically for the person class—by fusing standard RGB camera frames with Thermal (infrared) imagery. The fusion leverages the strengths of both modalities, making detection robust to varying lighting conditions, occlusions, and backgrounds.
The training process is based on Ultralytics YOLO and utilizes a 4-channel dataset (RGB + Thermal). Rather than modifying the YOLO architecture to natively accept 4 channels, the training pipeline uses an OpenCV-based preprocessing strategy to pre-fuse the multi-modal data into standard 3-channel (RGB) images that YOLO can readily digest.
The core of this approach lies in the preprocessing phase applied to every pair of RGB and Thermal images before they are fed into the model.
-
Reading Data:
- The RGB image is read as a 3-channel color image:
(H, W, 3). - The corresponding Thermal image is read as a 1-channel grayscale image:
(H, W).
- The RGB image is read as a 3-channel color image:
-
Spatial Alignment (Resizing):
- The Thermal array is resized to match the exact spatial dimensions
(W, H)of the RGB physical frame to ensure bounding box alignment between modalities.
- The Thermal array is resized to match the exact spatial dimensions
-
Normalization:
- To maximize contrast and ensure the thermal footprint spans the maximum pixel range, the Thermal image undergoes min-max normalization (
cv2.NORM_MINMAX), stretching intensities to0-255.
- To maximize contrast and ensure the thermal footprint spans the maximum pixel range, the Thermal image undergoes min-max normalization (
-
Channel Expansion:
- The normalized 1-channel Thermal image is duplicated across 3 channels, transforming it into a pseudo-RGB structure:
(H, W, 3).
- The normalized 1-channel Thermal image is duplicated across 3 channels, transforming it into a pseudo-RGB structure:
-
Weighted Additive Fusion:
- The dataset images are ultimately generated using a linear combination via
cv2.addWeighted. - Weighting: The RGB image holds a weight of
0.7(alpha), and the expanded Thermal image holds0.3(beta), with a scalar offset of0(gamma). Fused = (0.7 * RGB) + (0.3 * Thermal_3CH)- Why this ratio? This weighting retains the detailed structural and contextual textures provided by RGB while injecting enough thermal intensity (bright spots where body heat is detected) to highlight humans.
- The dataset images are ultimately generated using a linear combination via
The original bounding box coordinates (mapped around the humans in the frames) from the dataset are applied directly to the newly outputted fused frames. The resulting dataset pipeline trains a standard YOLO model configured with a single class (nc: 1, names: ['person']).
The training outputs the final PyTorch weights best.pt.
During inference, it is crucial that the model receives data identically processed to the training phase.
The custom inference.py script included in this repository enforces this:
- It accepts an arbitrary RGB image and its corresponding Thermal image.
- It dynamically runs the Fusion Preprocessing Algorithm (Normalizing, 3-channel Thermal expanding, and
0.7/0.3weighted fusion). - The YOLO model is loaded (
best.pt) and performs inference on the fused image. - The output is a single image containing the fused visualization overlaid with the YOLO bounding boxes highlighting detected persons.
This repo now includes a video inference pipeline that lets a user upload a video and get person-count stats.
video_inference.py: frame-by-frame person counting with YOLO.api.py: FastAPI server with upload endpoint.streamlit_app.py: Streamlit UI for upload, inference settings, and visual outputs.
pip install -r requirements.txt
streamlit run streamlit_app.pystreamlit_app.py now has 2 modes:
People Video (YOLO .pt): current video + thermal workflow.Plant Disease Images (.h5): upload a Keras.h5model and images for classification.
For .h5 mode, install TensorFlow if not already available:
pip install tensorflowpip install -r requirements.txt
uvicorn api:app --host 0.0.0.0 --port 8000 --reloadGET /healthPOST /infer/video(multipart form)
video(file, required): input video filethermal_video(file, optional): synchronized thermal video (falls back to RGB-only if omitted)frame_stride(int, optional, default5): process every Nth frameconfidence(float, optional, default0.45): YOLO confidence thresholdenable_tracking(bool, optional, defaulttrue): enable ByteTrack IDs and unique-person countsoutput_mode(string, optional, defaultstats): one ofstats,frames,video,allinclude_timeline(bool, optional, defaulttrue): include per-sampled-frame countssave_annotated(bool, optional, defaultfalse): save annotated output video
curl -X POST "http://localhost:8000/infer/video" \
-F "video=@sample.mp4" \
-F "thermal_video=@sample_thermal.mp4" \
-F "frame_stride=5" \
-F "confidence=0.45" \
-F "enable_tracking=true" \
-F "output_mode=frames" \
-F "include_timeline=true" \
-F "save_annotated=true"If output_mode=frames or all, response includes annotated_frame_urls.
In frames/all mode, frame exports are generated for every frame with bounding boxes.
If output_mode=video or all (or save_annotated=true), response includes annotated_video_url served under /outputs/....