Author Topic: Thermal image side by side with radiometric image?  (Read 7575 times)

0 Members and 1 Guest are viewing this topic.

Offline Abbott242Topic starter

  • Regular Contributor
  • *
  • Posts: 56
  • Country: ca
Thermal image side by side with radiometric image?
« on: February 08, 2024, 09:24:48 pm »
Hi all,

I'm trying to interface with a new core via UVC, but the results I'm getting... Well, speak for themselves. I assume this is a result of the core sending radiometric and video data at the same time? This was just via using the default Windows camera app for a quick test.

I would appreciate any help in trying to diagnose the problem, as this is a first for me. Thank you!

-Abbott242
 

Online Bud

  • Super Contributor
  • ***
  • Posts: 7928
  • Country: ca
Re: Thermal image side by side with radiometric image?
« Reply #1 on: February 08, 2024, 09:52:37 pm »
Try reversing the byte order in the right half of the image.
Facebook-free life and Rigol-free shack.
 

Offline Abbott242Topic starter

  • Regular Contributor
  • *
  • Posts: 56
  • Country: ca
Re: Thermal image side by side with radiometric image?
« Reply #2 on: February 09, 2024, 07:16:48 pm »
I'm not entirely sure what you mean... This is just a uvc camera - I don't think I can change each half of the image separately? But then again I'm not terribly experienced with software.
 

Offline LesioQ

  • Regular Contributor
  • *
  • Posts: 72
  • Country: pl
  • Every king should be naked.
Re: Thermal image side by side with radiometric image?
« Reply #3 on: February 24, 2024, 06:11:23 am »
 

Offline IR_Geek

  • Regular Contributor
  • *
  • Posts: 155
  • Country: us
Re: Thermal image side by side with radiometric image?
« Reply #4 on: February 24, 2024, 12:29:38 pm »
what size is the array supposed to be?   one picture you show is 3636x1496 and the other is 2720x1496  ;  what is the digital bit depth of the camera ... 8 bit, 12 bit, 14 bit?    You should be able to calculate your raw digital counts.   ignore the color as that is just a color map being applied.

if possible set the camera in RAW without a color map (or just grayscale) and look at the native bit depth.   0-255 ; 0-4095; 0-16383;  even better if the camera has a test pattern so you can verify output.
 

Offline IR_Geek

  • Regular Contributor
  • *
  • Posts: 155
  • Country: us
Re: Thermal image side by side with radiometric image?
« Reply #5 on: February 24, 2024, 12:38:26 pm »
Also what was mentioned above about byte order.   Another is where the 'data' is stored inside the stream.   for 8 bit it could be packed into a 16 bit stream.  Then it must be pulled apart and applied correctly.  Could still be byte swapped.   if it's 12 bit in a 16 bit then need to verify where the zero are located ... first 4 our last 4.   If the camera vendor is a real PITA then maybe 2 zeros in the front and 2 at the end.   

OR the other data location is where the temperature calibration is being stored if it doesn't have a header for each frame.
 

Offline figment

  • Contributor
  • Posts: 20
  • Country: us
Would this be a ICI Titan 1280 or Helios 640?
« Reply #6 on: March 01, 2024, 12:23:00 am »
What camera model are you working with? I have a USB thermal camera that gives the same mirrored double image.
 

Offline RRtheone

  • Regular Contributor
  • *
  • Posts: 113
  • Country: 00
Re: Thermal image side by side with radiometric image?
« Reply #7 on: May 15, 2026, 01:08:26 pm »
I recently connected an InfiRay LT Core to my phone. By chance, I noticed that it also displayed the same double green image when paired with a mobile app called “USB Camera”. Switching back to the InfiRay app restores normal video display.

1. The core may output split dual-stream video, which generic USB camera apps fail to separate correctly.
2. Raw thermal Y16 data is misinterpreted as RGB signals, leading to green tint and wrong colors.

 

Offline gamerpaddy

  • Frequent Contributor
  • **
  • Posts: 281
  • Country: de
Re: Thermal image side by side with radiometric image?
« Reply #8 on: May 15, 2026, 01:43:43 pm »
the core outputs YUV2 at double the resoluton since it only can do 8bit, but theres (usually) 14bit of data.
left side is chroma, right side is luma thats why it looks weird. its not rgb.
but actually its one image, right side is high byte, left side is low byte (or vise versa cant remember)

you have to merge both sides into one and then do false-color or range mapping since no display can show 14 bit grayscale

try
(its written for 1280x1040, you need to change all 1280, 2560 (double X res) and 1024 occurences to whatever your cam outputs.)


Code: [Select]
import cv2
import numpy as np
from datetime import datetime
import json
import os

# ---- enum ----

cam_index = 0
SETTINGS_FILE = "titan1280_settings.json"

# ---- color palettes for false color ----
def create_palettes():
    """Create various false color palettes (256 entries each)"""
    palettes = {}
    lut = np.arange(256, dtype=np.uint8).reshape(256, 1)
   
    # Grayscale (default)
    gray = np.zeros((256, 1, 3), dtype=np.uint8)
    for i in range(256):
        gray[i, 0] = [i, i, i]
    palettes['grayscale'] = gray
   
    # Jet/Rainbow
    palettes['jet'] = cv2.applyColorMap(lut, cv2.COLORMAP_JET)
   
    # Hot (black -> red -> yellow -> white)
    palettes['hot'] = cv2.applyColorMap(lut, cv2.COLORMAP_HOT)
   
    # Bone (blue-ish grayscale)
    palettes['bone'] = cv2.applyColorMap(lut, cv2.COLORMAP_BONE)
   
    # Inferno
    palettes['inferno'] = cv2.applyColorMap(lut, cv2.COLORMAP_INFERNO)
   
    # Viridis
    palettes['viridis'] = cv2.applyColorMap(lut, cv2.COLORMAP_VIRIDIS)
   
    # Plasma
    palettes['plasma'] = cv2.applyColorMap(lut, cv2.COLORMAP_PLASMA)
   
    # Turbo
    palettes['turbo'] = cv2.applyColorMap(lut, cv2.COLORMAP_TURBO)
   
    # Rainbow
    palettes['rainbow'] = cv2.applyColorMap(lut, cv2.COLORMAP_RAINBOW)
   
    # Ocean
    palettes['ocean'] = cv2.applyColorMap(lut, cv2.COLORMAP_OCEAN)
   
    return palettes

palettes = create_palettes()
palette_names = list(palettes.keys())
current_palette_index = 0
invert_palette = False  # Toggle for inverting the palette

# ---- Offset/Range controls ----
auto_range = True  # Auto-range enabled by default
manual_offset = 0  # 0-65535 for 16-bit
manual_range = 65535  # 1-65535 for 16-bit
show_histogram = True  # Toggle for histogram display
show_cursor_readout = True  # Toggle for pixel readout display

# ---- Auto-range percentile settings (adjustable) ----
AUTO_RANGE_LOW_PERCENTILE = 0.0   # Clip lowest 5% of pixels
AUTO_RANGE_HIGH_PERCENTILE = 100.0  # Clip highest 5% of pixels

# ---- Sharpening options (using Unsharp Mask - better for thermal) ----
sharpen_levels = ['Off', 'Low', 'Medium', 'High']
sharpen_index = 0  # Default: Off

# Unsharp mask parameters: (blur_size, strength)
# Uses Gaussian blur + weighted blend for clean edge enhancement
sharpen_params = {
    'Off': None,
    'Low': (5, 0.5),      # Subtle edge enhancement
    'Medium': (5, 1.0),    # Moderate sharpening
    'High': (7, 1.5),      # Strong sharpening
}

# ---- Settings persistence ----
def load_settings():
    """Load settings from JSON file"""
    global current_palette_index, invert_palette, auto_range, manual_offset, manual_range, show_histogram, sharpen_index, show_cursor_readout
    if os.path.exists(SETTINGS_FILE):
        try:
            with open(SETTINGS_FILE, 'r') as f:
                settings = json.load(f)
                current_palette_index = settings.get('palette_index', 0)
                invert_palette = settings.get('invert', False)
                auto_range = settings.get('auto_range', True)
                manual_offset = settings.get('offset', 0)
                manual_range = settings.get('range', 65535)
                show_histogram = settings.get('show_histogram', True)
                sharpen_index = settings.get('sharpen_index', 0)
                show_cursor_readout = settings.get('show_cursor_readout', True)
                print(f"Settings loaded from {SETTINGS_FILE}")
        except Exception as e:
            print(f"Could not load settings: {e}")

def save_settings():
    """Save current settings to JSON file"""
    settings = {
        'palette_index': current_palette_index,
        'invert': invert_palette,
        'auto_range': auto_range,
        'offset': manual_offset,
        'range': manual_range,
        'show_histogram': show_histogram,
        'sharpen_index': sharpen_index,
        'show_cursor_readout': show_cursor_readout
    }
    try:
        with open(SETTINGS_FILE, 'w') as f:
            json.dump(settings, f, indent=2)
        print(f"Settings saved to {SETTINGS_FILE}")
    except Exception as e:
        print(f"Could not save settings: {e}")

# Load settings at startup
load_settings()

# ---- button definitions ----
button_height = 30
button_width = 100
button_margin = 10
buttons = []

# ---- slider definitions ----
slider_width = 200
slider_height = 20
slider_track_height = 6
sliders = []
dragging_slider = None  # Track which slider is being dragged

def create_buttons():
    """Create button definitions"""
    global buttons
    buttons = [
        # Row 1: Image controls
        {'name': 'Save TIF', 'x': button_margin, 'y': button_margin, 'w': button_width, 'h': button_height, 'action': 'save_tif'},
        {'name': 'Save PNG', 'x': button_margin + button_width + button_margin, 'y': button_margin, 'w': button_width, 'h': button_height, 'action': 'save_png'},
        {'name': 'Palette', 'x': button_margin + 2 * (button_width + button_margin), 'y': button_margin, 'w': button_width, 'h': button_height, 'action': 'cycle_palette'},
        {'name': 'Invert', 'x': button_margin + 3 * (button_width + button_margin), 'y': button_margin, 'w': button_width, 'h': button_height, 'action': 'toggle_invert'},
        {'name': 'Auto Range', 'x': button_margin + 4 * (button_width + button_margin), 'y': button_margin, 'w': button_width, 'h': button_height, 'action': 'toggle_autorange'},
        {'name': 'Histogram', 'x': button_margin + 5 * (button_width + button_margin), 'y': button_margin, 'w': button_width, 'h': button_height, 'action': 'toggle_histogram'},
        {'name': 'Sharpen', 'x': button_margin + 6 * (button_width + button_margin), 'y': button_margin, 'w': button_width, 'h': button_height, 'action': 'cycle_sharpen'},
    ]

def create_sliders():
    """Create slider definitions"""
    global sliders
    slider_y_start = button_margin + button_height + button_margin
    sliders = [
        {'name': 'Offset', 'x': button_margin, 'y': slider_y_start, 'w': slider_width, 'h': slider_height, 'min': 0, 'max': 65535, 'value': 'manual_offset'},
        {'name': 'Range', 'x': button_margin + slider_width + 60, 'y': slider_y_start, 'w': slider_width, 'h': slider_height, 'min': 1, 'max': 65535, 'value': 'manual_range'},
    ]

create_buttons()
create_sliders()

# Global state
current_gray16 = None
current_preview = None  # Clean preview without UI elements (for PNG saving)
mouse_x, mouse_y = -1, -1  # Current mouse position
cursor_radius = 10  # Radius for right-click cursor region (adjustable via scroll)

def update_slider_value(slider, x):
    """Update slider value based on x position"""
    global manual_offset, manual_range
    # Calculate relative position
    rel_x = max(0, min(x - slider['x'], slider['w']))
    ratio = rel_x / slider['w']
    value = int(slider['min'] + ratio * (slider['max'] - slider['min']))
    value = max(slider['min'], min(slider['max'], value))
   
    if slider['value'] == 'manual_offset':
        manual_offset = value
    elif slider['value'] == 'manual_range':
        manual_range = max(1, value)  # Ensure range is at least 1

def mouse_callback(event, x, y, flags, param):
    global current_palette_index, current_gray16, current_preview
    global invert_palette, auto_range, dragging_slider, show_histogram, sharpen_index
    global mouse_x, mouse_y, show_cursor_readout
   
    # Always track mouse position for pixel value display
    mouse_x, mouse_y = x, y
   
    if event == cv2.EVENT_LBUTTONDOWN:
        # Check buttons first
        for btn in buttons:
            if btn['x'] <= x <= btn['x'] + btn['w'] and btn['y'] <= y <= btn['y'] + btn['h']:
                if btn['action'] == 'save_tif':
                    if current_gray16 is not None:
                        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                        filename = f"capture_{timestamp}.tif"
                        cv2.imwrite(filename, current_gray16)
                        print(f"Saved raw 16-bit TIF: {filename}")
                elif btn['action'] == 'save_png':
                    if current_preview is not None:
                        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                        filename = f"capture_{timestamp}.png"
                        cv2.imwrite(filename, current_preview)
                        print(f"Saved PNG: {filename}")
                elif btn['action'] == 'cycle_palette':
                    current_palette_index = (current_palette_index + 1) % len(palette_names)
                    print(f"Palette: {palette_names[current_palette_index]}")
                elif btn['action'] == 'toggle_invert':
                    invert_palette = not invert_palette
                    print(f"Invert: {'ON' if invert_palette else 'OFF'}")
                elif btn['action'] == 'toggle_autorange':
                    auto_range = not auto_range
                    print(f"Auto Range: {'ON' if auto_range else 'OFF'}")
                elif btn['action'] == 'toggle_histogram':
                    show_histogram = not show_histogram
                    print(f"Histogram: {'ON' if show_histogram else 'OFF'}")
                elif btn['action'] == 'cycle_sharpen':
                    sharpen_index = (sharpen_index + 1) % len(sharpen_levels)
                    print(f"Sharpen: {sharpen_levels[sharpen_index]}")
                return
       
        # Check sliders (only if auto_range is disabled)
        if not auto_range:
            for slider in sliders:
                if slider['x'] <= x <= slider['x'] + slider['w'] and slider['y'] <= y <= slider['y'] + slider['h']:
                    dragging_slider = slider
                    update_slider_value(slider, x)
                    return
   
    elif event == cv2.EVENT_MOUSEMOVE:
        if dragging_slider is not None and not auto_range:
            update_slider_value(dragging_slider, x)
   
    elif event == cv2.EVENT_LBUTTONUP:
        dragging_slider = None
   
    elif event == cv2.EVENT_RBUTTONDOWN:
        # Toggle cursor readout with right click
        show_cursor_readout = not show_cursor_readout
        print(f"Cursor readout: {'ON' if show_cursor_readout else 'OFF'}")
   
    elif event == cv2.EVENT_MOUSEWHEEL:
        # Adjust cursor radius with scroll wheel (when cursor readout is enabled)
        global cursor_radius
        if show_cursor_readout:
            if flags > 0:  # Scroll up
                cursor_radius = min(50, cursor_radius + 1)
            else:  # Scroll down
                cursor_radius = max(1, cursor_radius - 1)
            print(f"Cursor radius: {cursor_radius}")

def draw_buttons(img):
    """Draw buttons on the image"""
    for btn in buttons:
        # Determine button state color
        bg_color = (60, 60, 60)
        if btn['action'] == 'toggle_invert' and invert_palette:
            bg_color = (0, 100, 0)  # Green when active
        elif btn['action'] == 'toggle_autorange' and auto_range:
            bg_color = (0, 100, 0)  # Green when active
        elif btn['action'] == 'toggle_histogram' and show_histogram:
            bg_color = (0, 100, 0)  # Green when active
        elif btn['action'] == 'cycle_sharpen' and sharpen_index > 0:
            bg_color = (0, 100, 0)  # Green when active (any level except Off)
       
        # Button background
        cv2.rectangle(img, (btn['x'], btn['y']), (btn['x'] + btn['w'], btn['y'] + btn['h']), bg_color, -1)
        # Button border
        cv2.rectangle(img, (btn['x'], btn['y']), (btn['x'] + btn['w'], btn['y'] + btn['h']), (120, 120, 120), 1)
       
        # Button text
        text = btn['name']
        if btn['action'] == 'cycle_palette':
            text = palette_names[current_palette_index].capitalize()
        elif btn['action'] == 'toggle_invert':
            text = 'Invert ON' if invert_palette else 'Invert OFF'
        elif btn['action'] == 'toggle_autorange':
            text = 'Auto ON' if auto_range else 'Auto OFF'
        elif btn['action'] == 'toggle_histogram':
            text = 'Hist ON' if show_histogram else 'Hist OFF'
        elif btn['action'] == 'cycle_sharpen':
            text = f"Sharp: {sharpen_levels[sharpen_index]}"
       
        text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)[0]
        text_x = btn['x'] + (btn['w'] - text_size[0]) // 2
        text_y = btn['y'] + (btn['h'] + text_size[1]) // 2
        cv2.putText(img, text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1)
   
    return img

def draw_sliders(img):
    """Draw sliders on the image (only visible when auto_range is off)"""
    if auto_range:
        return img
   
    for slider in sliders:
        # Get current value
        if slider['value'] == 'manual_offset':
            current_val = manual_offset
        else:
            current_val = manual_range
       
        # Calculate handle position
        ratio = (current_val - slider['min']) / (slider['max'] - slider['min'])
        handle_x = int(slider['x'] + ratio * slider['w'])
       
        # Draw slider label
        label = f"{slider['name']}: {current_val}"
        cv2.putText(img, label, (slider['x'], slider['y'] - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (255, 255, 255), 1)
       
        # Draw track background
        track_y = slider['y'] + (slider['h'] - slider_track_height) // 2
        cv2.rectangle(img, (slider['x'], track_y), (slider['x'] + slider['w'], track_y + slider_track_height), (40, 40, 40), -1)
        cv2.rectangle(img, (slider['x'], track_y), (slider['x'] + slider['w'], track_y + slider_track_height), (80, 80, 80), 1)
       
        # Draw filled portion
        cv2.rectangle(img, (slider['x'], track_y), (handle_x, track_y + slider_track_height), (100, 150, 100), -1)
       
        # Draw handle
        handle_radius = slider['h'] // 2
        cv2.circle(img, (handle_x, slider['y'] + slider['h'] // 2), handle_radius, (200, 200, 200), -1)
        cv2.circle(img, (handle_x, slider['y'] + slider['h'] // 2), handle_radius, (255, 255, 255), 1)
   
    return img

# ---- Histogram settings ----
HIST_WIDTH = 256
HIST_HEIGHT = 128
HIST_BINS = 256  # Number of bins for 16-bit data (downsampled for display)

def draw_histogram(img, gray16):
    """Draw a histogram of the 16-bit image data in the bottom-right corner"""
    img_h, img_w = img.shape[:2]
   
    # Position histogram in bottom-right with margin
    margin = 10
    hist_x = img_w - HIST_WIDTH - margin
    hist_y = img_h - HIST_HEIGHT - margin
   
    # Calculate histogram with 256 bins for 16-bit data (0-65535)
    hist, _ = np.histogram(gray16.flatten(), bins=HIST_BINS, range=(0, 65536))
   
    # Normalize histogram to fit in display height
    if hist.max() > 0:
        hist_normalized = (hist / hist.max() * (HIST_HEIGHT - 20)).astype(np.int32)
    else:
        hist_normalized = np.zeros(HIST_BINS, dtype=np.int32)
   
    # Draw semi-transparent background
    overlay = img.copy()
    cv2.rectangle(overlay, (hist_x - 5, hist_y - 20), (hist_x + HIST_WIDTH + 5, hist_y + HIST_HEIGHT + 5), (30, 30, 30), -1)
    cv2.addWeighted(overlay, 0.7, img, 0.3, 0, img)
   
    # Draw border
    cv2.rectangle(img, (hist_x - 5, hist_y - 20), (hist_x + HIST_WIDTH + 5, hist_y + HIST_HEIGHT + 5), (80, 80, 80), 1)
   
    # Draw histogram title
    cv2.putText(img, "Histogram (16-bit)", (hist_x, hist_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (200, 200, 200), 1)
   
    # Draw histogram bars
    for i in range(HIST_BINS):
        bar_height = hist_normalized[i]
        if bar_height > 0:
            x = hist_x + i
            y_bottom = hist_y + HIST_HEIGHT
            y_top = y_bottom - bar_height
            cv2.line(img, (x, y_bottom), (x, y_top), (100, 200, 100), 1)
   
    # Draw axis labels
    cv2.putText(img, "0", (hist_x, hist_y + HIST_HEIGHT + 12), cv2.FONT_HERSHEY_SIMPLEX, 0.25, (150, 150, 150), 1)
    cv2.putText(img, "65535", (hist_x + HIST_WIDTH - 25, hist_y + HIST_HEIGHT + 12), cv2.FONT_HERSHEY_SIMPLEX, 0.25, (150, 150, 150), 1)
   
    # Draw min/max/mean info
    min_val = int(gray16.min())
    max_val = int(gray16.max())
    mean_val = int(gray16.mean())
    info_text = f"Min:{min_val} Max:{max_val} Mean:{mean_val}"
    cv2.putText(img, info_text, (hist_x, hist_y + HIST_HEIGHT + 22), cv2.FONT_HERSHEY_SIMPLEX, 0.28, (180, 180, 180), 1)
   
    # Draw markers for current offset/range if manual mode
    if not auto_range:
        # Calculate bin position (align with histogram bars)
        bin_width = HIST_WIDTH / HIST_BINS
       
        # Draw offset marker (red line) - at the start of the offset bin
        offset_bin = int(manual_offset / 65536 * HIST_BINS)
        offset_x = hist_x + int(offset_bin * bin_width)
        cv2.line(img, (offset_x, hist_y), (offset_x, hist_y + HIST_HEIGHT), (0, 0, 255), 1)
       
        # Draw range end marker (blue line) - at the end of the range bin
        range_end = min(manual_offset + manual_range, 65535)
        range_bin = int(range_end / 65536 * HIST_BINS)
        range_x = hist_x + int(range_bin * bin_width)
        cv2.line(img, (range_x, hist_y), (range_x, hist_y + HIST_HEIGHT), (255, 100, 100), 1)
   
    return img

def draw_pixel_values(img, gray16, mx, my, radius=10, low_byte=None, high_byte=None):
    """Draw pixel values in a radius around the mouse cursor
   
    Shows 16-bit pixel values from gray16 as an overlay near the cursor.
    Only displays when mouse is within the image bounds.
    Also shows the raw high and low bytes for debugging.
    """
    img_h, img_w = img.shape[:2]
    g16_h, g16_w = gray16.shape[:2]
   
    # Check if mouse is within image bounds (use gray16 dimensions)
    if mx < 0 or my < 0 or mx >= g16_w or my >= g16_h:
        return img
   
    # Calculate the area to sample (10 pixel radius = 21x21 grid)
    x_start = max(0, mx - radius)
    x_end = min(g16_w, mx + radius + 1)
    y_start = max(0, my - radius)
    y_end = min(g16_h, my + radius + 1)
   
    # Get statistics for the region from the 16-bit data
    region = gray16[y_start:y_end, x_start:x_end].astype(np.float64)
    if region.size == 0:
        return img
   
    # Use the actual 16-bit values
    center_val = int(gray16[my, mx])
    region_min = int(region.min())
    region_max = int(region.max())
    region_mean = float(region.mean())
    region_std = float(region.std())
   
    # Get raw high/low byte values for debugging
    high_val = int(high_byte[my, mx]) if high_byte is not None else 0
    low_val = int(low_byte[my, mx]) if low_byte is not None else 0
   
    # Prepare info text lines
    info_lines = [
        f"Pos: ({mx}, {my})",
        f"16bit: {center_val}",
        f"High: {high_val} Low: {low_val}",
        f"Region {2*radius+1}x{2*radius+1}:",
        f"  Min: {region_min}",
        f"  Max: {region_max}",
        f"  Mean: {region_mean:.1f}",
        f"  Std: {region_std:.1f}",
    ]
   
    # Calculate box dimensions
    font = cv2.FONT_HERSHEY_SIMPLEX
    font_scale = 0.35
    thickness = 1
    line_height = 14
    padding = 5
   
    max_text_width = 0
    for line in info_lines:
        text_size = cv2.getTextSize(line, font, font_scale, thickness)[0]
        max_text_width = max(max_text_width, text_size[0])
   
    box_width = max_text_width + 2 * padding
    box_height = len(info_lines) * line_height + 2 * padding
   
    # Position the box near the cursor (offset to avoid covering the area)
    box_x = mx + 20
    box_y = my - box_height // 2
   
    # Ensure box stays within image bounds
    if box_x + box_width > img_w:
        box_x = mx - box_width - 20
    if box_y < 0:
        box_y = 0
    if box_y + box_height > img_h:
        box_y = img_h - box_height
   
    # Draw semi-transparent background
    overlay = img.copy()
    cv2.rectangle(overlay, (box_x, box_y), (box_x + box_width, box_y + box_height), (30, 30, 30), -1)
    cv2.addWeighted(overlay, 0.8, img, 0.2, 0, img)
   
    # Draw border
    cv2.rectangle(img, (box_x, box_y), (box_x + box_width, box_y + box_height), (100, 100, 100), 1)
   
    # Draw text lines
    for i, line in enumerate(info_lines):
        text_y = box_y + padding + (i + 1) * line_height - 3
        cv2.putText(img, line, (box_x + padding, text_y), font, font_scale, (255, 255, 255), thickness)
   
    # Draw cursor crosshair
    cv2.line(img, (mx - 5, my), (mx + 5, my), (0, 255, 255), 1)
    cv2.line(img, (mx, my - 5), (mx, my + 5), (0, 255, 255), 1)
   
    # Draw circle showing the sampled region
    cv2.circle(img, (mx, my), radius, (0, 255, 255), 1)
   
    return img

def apply_sharpening(img):
    """Apply unsharp mask sharpening to the image based on current setting
   
    Unsharp masking: sharpened = original + strength * (original - blurred)
    This method enhances edges without amplifying noise like kernel sharpening.
    """
    level = sharpen_levels[sharpen_index]
    params = sharpen_params[level]
   
    if params is None:
        return img
   
    blur_size, strength = params
   
    # Apply Gaussian blur
    blurred = cv2.GaussianBlur(img, (blur_size, blur_size), 0)
   
    # Unsharp mask: original + strength * (original - blurred)
    # Using float to avoid overflow
    sharpened = img.astype(np.float32) + strength * (img.astype(np.float32) - blurred.astype(np.float32))
   
    # Clip to valid range
    sharpened = np.clip(sharpened, 0, 255).astype(np.uint8)
    return sharpened

def apply_palette(gray8):
    """Apply current palette to grayscale image (with optional inversion)"""
    # Apply inversion if enabled
    img = 255 - gray8 if invert_palette else gray8
   
    palette_name = palette_names[current_palette_index]
    if palette_name == 'grayscale':
        return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
    else:
        return cv2.applyColorMap(img, getattr(cv2, f'COLORMAP_{palette_name.upper()}'))

# ---- open ----
cap = cv2.VideoCapture(cam_index, cv2.CAP_MSMF)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 2560)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1024)

# Create window and set mouse callback
WINDOW_NAME = "16bit preview"
cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_AUTOSIZE)
cv2.setMouseCallback(WINDOW_NAME, mouse_callback)

# ---- loop ----
while True:
    ret, frame = cap.read()
    if not ret:
        break
   
    # Check if window was closed (X button)
    if cv2.getWindowProperty(WINDOW_NAME, cv2.WND_PROP_VISIBLE) < 1:
        break

    h, w, c = frame.shape  # this is BGR (driver converted)

    # split halves
    b1  = frame[:, :1280, :]
    b2 = frame[:, 1280:, :]

    # first half uses BLUE, second uses GREEN
    low  = b2[:, :, 1].astype(np.uint16)
    high = b1[:, :, 1].astype(np.uint16)

    gray16 = (high << 8) | low
    current_gray16 = gray16.copy()

    # Apply normalization (auto or manual)
    if auto_range:
        # Use percentile-based normalization to clip outliers
        low_val = np.percentile(gray16, AUTO_RANGE_LOW_PERCENTILE)
        high_val = np.percentile(gray16, AUTO_RANGE_HIGH_PERCENTILE)
       
        # Ensure we have a valid range
        if high_val <= low_val:
            high_val = low_val + 1
       
        # Normalize using the percentile range
        gray_float = gray16.astype(np.float32)
        gray_float = (gray_float - low_val) / (high_val - low_val) * 255.0
        gray_float = np.clip(gray_float, 0, 255)
        preview8 = gray_float.astype(np.uint8)
    else:
        # Manual offset and range
        # Clip values to the offset/range window and scale to 0-255
        gray_float = gray16.astype(np.float32)
        gray_float = (gray_float - manual_offset) / manual_range * 255.0
        gray_float = np.clip(gray_float, 0, 255)
        preview8 = gray_float.astype(np.uint8)
   
    # Apply sharpening (before color mapping for better results)
    preview8 = apply_sharpening(preview8)
   
    # Apply false color palette
    preview = apply_palette(preview8)
   
    # Store clean preview without UI elements (for PNG saving)
    current_preview = preview.copy()
   
    # Draw UI elements (only for display, not saved)
    preview = draw_buttons(preview)
    preview = draw_sliders(preview)
    if show_histogram:
        preview = draw_histogram(preview, gray16)
   
    # Draw pixel values around mouse cursor (if enabled)
    if show_cursor_readout:
        preview = draw_pixel_values(preview, gray16, mouse_x, mouse_y, radius=cursor_radius, low_byte=low, high_byte=high)
   
    cv2.imshow(WINDOW_NAME, preview)

    key = cv2.waitKey(1) & 0xFF
    if key == 27:  # ESC key
        break

# Save settings before exit
save_settings()

cap.release()
cv2.destroyAllWindows()


i tried getting that to run on android with some python apps but it didnt work  you might need to write an app for that.
 
The following users thanked this post: RRtheone


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf

 

-->