Improve stream UI: grouped sections, full-size preview lightbox, and test redesign
- Separate Screen Capture and Processed streams into grouped sections with headers - Remove redundant Type dropdown from stream modal (type inferred from add button) - Add full-resolution image to test endpoints alongside thumbnails - Add image lightbox with clickable preview for full-size viewing - Move test results from modal into lightbox overlay with capture stats - Apply postprocessing to both thumbnail and full image for processed streams - Rename "Assigned Picture Stream" to "Picture Stream" in device settings - Fix null reference errors from removed test result HTML elements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1052,12 +1052,19 @@ async def test_template(
|
||||
thumbnail = pil_image.copy()
|
||||
thumbnail.thumbnail((thumbnail_width, thumbnail_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Encode full capture thumbnail as JPEG
|
||||
# Encode thumbnail as JPEG
|
||||
img_buffer = io.BytesIO()
|
||||
thumbnail.save(img_buffer, format='JPEG', quality=85)
|
||||
img_buffer.seek(0)
|
||||
full_capture_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
|
||||
full_capture_data_uri = f"data:image/jpeg;base64,{full_capture_b64}"
|
||||
thumbnail_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
|
||||
thumbnail_data_uri = f"data:image/jpeg;base64,{thumbnail_b64}"
|
||||
|
||||
# Encode full-resolution image as JPEG
|
||||
full_buffer = io.BytesIO()
|
||||
pil_image.save(full_buffer, format='JPEG', quality=90)
|
||||
full_buffer.seek(0)
|
||||
full_b64 = base64.b64encode(full_buffer.getvalue()).decode('utf-8')
|
||||
full_data_uri = f"data:image/jpeg;base64,{full_b64}"
|
||||
|
||||
# Calculate metrics
|
||||
actual_fps = frame_count / actual_duration if actual_duration > 0 else 0
|
||||
@@ -1067,7 +1074,8 @@ async def test_template(
|
||||
|
||||
return TemplateTestResponse(
|
||||
full_capture=CaptureImage(
|
||||
image=full_capture_data_uri,
|
||||
image=thumbnail_data_uri,
|
||||
full_image=full_data_uri,
|
||||
width=width,
|
||||
height=height,
|
||||
thumbnail_width=thumbnail_width,
|
||||
@@ -1469,34 +1477,41 @@ async def test_picture_stream(
|
||||
thumbnail = pil_image.copy()
|
||||
thumbnail.thumbnail((thumbnail_width, thumbnail_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Apply postprocessing to preview if this is a processed stream
|
||||
# Apply postprocessing if this is a processed stream
|
||||
pp_template_ids = chain["postprocessing_template_ids"]
|
||||
if pp_template_ids:
|
||||
try:
|
||||
pp = pp_store.get_template(pp_template_ids[0])
|
||||
img_array = np.array(thumbnail, dtype=np.float32) / 255.0
|
||||
|
||||
if pp.brightness != 1.0:
|
||||
img_array *= pp.brightness
|
||||
def apply_pp(img):
|
||||
arr = np.array(img, dtype=np.float32) / 255.0
|
||||
if pp.brightness != 1.0:
|
||||
arr *= pp.brightness
|
||||
if pp.saturation != 1.0:
|
||||
lum = np.dot(arr[..., :3], [0.299, 0.587, 0.114])[..., np.newaxis]
|
||||
arr[..., :3] = lum + (arr[..., :3] - lum) * pp.saturation
|
||||
if pp.gamma != 1.0:
|
||||
arr = np.power(np.clip(arr, 0, 1), 1.0 / pp.gamma)
|
||||
return Image.fromarray(np.clip(arr * 255.0, 0, 255).astype(np.uint8))
|
||||
|
||||
if pp.saturation != 1.0:
|
||||
luminance = np.dot(img_array[..., :3], [0.299, 0.587, 0.114])
|
||||
luminance = luminance[..., np.newaxis]
|
||||
img_array[..., :3] = luminance + (img_array[..., :3] - luminance) * pp.saturation
|
||||
|
||||
if pp.gamma != 1.0:
|
||||
img_array = np.power(np.clip(img_array, 0, 1), 1.0 / pp.gamma)
|
||||
|
||||
img_array = np.clip(img_array * 255.0, 0, 255).astype(np.uint8)
|
||||
thumbnail = Image.fromarray(img_array)
|
||||
thumbnail = apply_pp(thumbnail)
|
||||
pil_image = apply_pp(pil_image)
|
||||
except ValueError:
|
||||
logger.warning(f"PP template {pp_template_ids[0]} not found, skipping postprocessing preview")
|
||||
|
||||
# Encode thumbnail
|
||||
img_buffer = io.BytesIO()
|
||||
thumbnail.save(img_buffer, format='JPEG', quality=85)
|
||||
img_buffer.seek(0)
|
||||
full_capture_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
|
||||
full_capture_data_uri = f"data:image/jpeg;base64,{full_capture_b64}"
|
||||
thumbnail_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
|
||||
thumbnail_data_uri = f"data:image/jpeg;base64,{thumbnail_b64}"
|
||||
|
||||
# Encode full-resolution image
|
||||
full_buffer = io.BytesIO()
|
||||
pil_image.save(full_buffer, format='JPEG', quality=90)
|
||||
full_buffer.seek(0)
|
||||
full_b64 = base64.b64encode(full_buffer.getvalue()).decode('utf-8')
|
||||
full_data_uri = f"data:image/jpeg;base64,{full_b64}"
|
||||
|
||||
actual_fps = frame_count / actual_duration if actual_duration > 0 else 0
|
||||
avg_capture_time_ms = (total_capture_time / frame_count * 1000) if frame_count > 0 else 0
|
||||
@@ -1504,7 +1519,8 @@ async def test_picture_stream(
|
||||
|
||||
return TemplateTestResponse(
|
||||
full_capture=CaptureImage(
|
||||
image=full_capture_data_uri,
|
||||
image=thumbnail_data_uri,
|
||||
full_image=full_data_uri,
|
||||
width=width,
|
||||
height=height,
|
||||
thumbnail_width=thumbnail_width,
|
||||
|
||||
@@ -292,9 +292,10 @@ class TemplateTestRequest(BaseModel):
|
||||
class CaptureImage(BaseModel):
|
||||
"""Captured image with metadata."""
|
||||
|
||||
image: str = Field(description="Base64-encoded image data")
|
||||
width: int = Field(description="Image width in pixels")
|
||||
height: int = Field(description="Image height in pixels")
|
||||
image: str = Field(description="Base64-encoded thumbnail image data")
|
||||
full_image: Optional[str] = Field(None, description="Base64-encoded full-resolution image data")
|
||||
width: int = Field(description="Original image width in pixels")
|
||||
height: int = Field(description="Original image height in pixels")
|
||||
thumbnail_width: Optional[int] = Field(None, description="Thumbnail width (if resized)")
|
||||
thumbnail_height: Optional[int] = Field(None, description="Thumbnail height (if resized)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user