arguments to force 16 or 8 bit color

This commit is contained in:
Nathan
2025-12-11 14:56:14 -07:00
parent 0b3a1b4d1a
commit 8d7750a78f
2 changed files with 624 additions and 47 deletions

View File

@@ -2508,3 +2508,507 @@ This format is used for elapsed time, ETA, and total time in the summary.
--- ---
_**User (2025-12-11 21:39Z)**_
let's add options to force 8bit or 16bit color: --16bit --8bit; shorthand -16, -8
---
_**Agent (model default, mode Agent)**_
---
Adding command-line options to force 8-bit or 16-bit color depth:
---
<tool-use data-tool-type="read" data-tool-name="read_file">
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="read" data-tool-name="read_file">
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
</details>
</tool-use>
---
---
---
<tool-use data-tool-type="write" data-tool-name="search_replace">
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
**Chunk 1**
Lines added: 1, lines removed: undefined
```diff
@@ -7,4 +7,5 @@
import os
import sys
+ import argparse
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor, as_completed
```
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="search_replace">
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
**Chunk 1**
Lines added: 8, lines removed: 2
```diff
@@ -14,6 +14,12 @@
import time
- def compress_png(input_path, output_path):
- """Compress a single PNG file."""
+ def compress_png(input_path, output_path, force_bitdepth=None):
+ """Compress a single PNG file.
+
+ Args:
+ input_path: Path to input image
+ output_path: Path to output image
+ force_bitdepth: None (auto-detect), '8' (force 8-bit), or '16' (force 16-bit)
+ """
try:
input_path = Path(input_path)
```
**Chunk 2**
Lines added: 30, lines removed: 26
```diff
@@ -34,31 +40,35 @@
img = Image.open(input_path)
- # Check if image is 16-bit
- # PIL represents 16-bit grayscale as 'I' mode (32-bit integer)
- # For color images, check pixel values to determine bit depth
- is_16bit = False
- original_mode = img.mode
-
- if img.mode == 'I':
- # 'I' mode typically represents 16-bit grayscale
- is_16bit = True
- elif img.mode in ('RGB', 'RGBA', 'LA'):
- # Check if max pixel value exceeds 8-bit range
- try:
- # Get a sample of pixels to check
- pixels = list(img.getdata())
- if pixels:
- # Flatten if needed (for multi-channel modes)
- if isinstance(pixels[0], (tuple, list)):
- max_val = max(max(p) for p in pixels[:1000]) # Sample first 1000 pixels
- else:
- max_val = max(pixels[:1000])
- if max_val &gt; 255:
- is_16bit = True
- except:
- # If we can't determine, assume 8-bit to be safe
- pass
+ # Determine target bit depth
+ if force_bitdepth == '8':
+ is_16bit = False
+ elif force_bitdepth == '16':
+ is_16bit = True
+ else:
+ # Auto-detect bit depth
+ is_16bit = False
+ original_mode = img.mode
+
+ if img.mode == 'I':
+ # 'I' mode typically represents 16-bit grayscale
+ is_16bit = True
+ elif img.mode in ('RGB', 'RGBA', 'LA'):
+ # Check if max pixel value exceeds 8-bit range
+ try:
+ # Get a sample of pixels to check
+ pixels = list(img.getdata())
+ if pixels:
+ # Flatten if needed (for multi-channel modes)
+ if isinstance(pixels[0], (tuple, list)):
+ max_val = max(max(p) for p in pixels[:1000]) # Sample first 1000 pixels
+ else:
+ max_val = max(pixels[:1000])
+ if max_val &gt; 255:
+ is_16bit = True
+ except:
+ # If we can't determine, assume 8-bit to be safe
+ pass
- # Preserve 16-bit depth if present, otherwise convert as needed
+ # Handle bit depth conversion based on target
if is_16bit:
# For 16-bit images, preserve the mode
```
</details>
</tool-use>
---
<tool-use data-tool-type="read" data-tool-name="read_file">
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
</details>
</tool-use>
---
---
Updating the conversion logic for forced bit depths and adding argument parsing:
---
<tool-use data-tool-type="write" data-tool-name="search_replace">
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
**Chunk 1**
Lines added: 52, lines removed: 22
```diff
@@ -72,26 +72,56 @@
# Handle bit depth conversion based on target
if is_16bit:
- # For 16-bit images, preserve the mode
- # PIL will save 16-bit when mode is 'I' (grayscale) or when using specific modes
- if img.mode == 'I':
- # Keep 16-bit grayscale
- pass
- elif img.mode in ('RGB', 'RGBA'):
- # Keep color mode - PIL may preserve 16-bit depending on how it was loaded
- # Note: PIL's PNG save may convert 16-bit RGB to 8-bit, but we preserve the mode
- pass
- else:
- # Convert other modes while trying to preserve bit depth
- if 'A' in img.mode:
- img = img.convert('RGBA')
- else:
- img = img.convert('RGB')
- else:
- # 8-bit images: convert as needed
- if img.mode == 'RGBA':
- # Keep RGBA for transparency support
- img = img.convert('RGBA')
- elif img.mode != 'RGB':
- img = img.convert('RGB')
+ # Force or preserve 16-bit
+ if force_bitdepth == '16':
+ # Force 16-bit: convert to appropriate 16-bit mode
+ if img.mode == 'I':
+ # Already 16-bit grayscale
+ pass
+ elif 'A' in img.mode or img.mode == 'LA':
+ # Convert to 16-bit RGBA (PIL limitation: may not fully preserve 16-bit color)
+ img = img.convert('RGBA')
+ else:
+ # Convert to 16-bit RGB
+ img = img.convert('RGB')
+ else:
+ # Preserve existing 16-bit
+ if img.mode == 'I':
+ # Keep 16-bit grayscale
+ pass
+ elif img.mode in ('RGB', 'RGBA'):
+ # Keep color mode - PIL may preserve 16-bit depending on how it was loaded
+ pass
+ else:
+ # Convert other modes while trying to preserve bit depth
+ if 'A' in img.mode:
+ img = img.convert('RGBA')
+ else:
+ img = img.convert('RGB')
+ else:
+ # Force or use 8-bit
+ if force_bitdepth == '8':
+ # Force 8-bit: ensure we're in 8-bit mode
+ if img.mode == 'I':
+ # Convert 16-bit grayscale to 8-bit
+ img = img.convert('L')
+ elif img.mode == 'RGBA':
+ # Keep RGBA for transparency support (8-bit)
+ img = img.convert('RGBA')
+ elif img.mode == 'RGB':
+ # Already 8-bit RGB
+ pass
+ else:
+ # Convert to 8-bit RGB
+ if 'A' in img.mode:
+ img = img.convert('RGBA')
+ else:
+ img = img.convert('RGB')
+ else:
+ # 8-bit images: convert as needed
+ if img.mode == 'RGBA':
+ # Keep RGBA for transparency support
+ img = img.convert('RGBA')
+ elif img.mode != 'RGB':
+ img = img.convert('RGB')
# Save with maximum compression
```
</details>
</tool-use>
---
<tool-use data-tool-type="read" data-tool-name="read_file">
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
</details>
</tool-use>
---
---
Updating the main function to parse arguments and pass the bitdepth option:
---
<tool-use data-tool-type="read" data-tool-name="read_file">
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
</details>
</tool-use>
---
---
---
<tool-use data-tool-type="write" data-tool-name="search_replace">
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
**Chunk 1**
Lines added: 32, lines removed: undefined
```diff
@@ -181,4 +181,36 @@
def main():
+ # Parse command-line arguments
+ parser = argparse.ArgumentParser(
+ description='Multithreaded PNG compression script with maximum parallelism.'
+ )
+ parser.add_argument(
+ '--8bit', '-8',
+ action='store_true',
+ dest='force_8bit',
+ help='Force 8-bit color depth for all images'
+ )
+ parser.add_argument(
+ '--16bit', '-16',
+ action='store_true',
+ dest='force_16bit',
+ help='Force 16-bit color depth for all images'
+ )
+ args = parser.parse_args()
+
+ # Determine bit depth setting
+ if args.force_8bit and args.force_16bit:
+ print("Error: Cannot specify both --8bit and --16bit. Choose one.")
+ return
+ elif args.force_8bit:
+ force_bitdepth = '8'
+ print("Mode: Forcing 8-bit color depth")
+ elif args.force_16bit:
+ force_bitdepth = '16'
+ print("Mode: Forcing 16-bit color depth")
+ else:
+ force_bitdepth = None
+ print("Mode: Auto-detect bit depth (preserve 16-bit if present)")
+
input_dir = Path('input')
output_dir = Path('output')
```
</details>
</tool-use>
---
<tool-use data-tool-type="read" data-tool-name="read_file">
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
</details>
</tool-use>
---
---
---
<tool-use data-tool-type="read" data-tool-name="read_file">
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
</details>
</tool-use>
---
---
---
<tool-use data-tool-type="write" data-tool-name="search_replace">
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
**Chunk 1**
Lines added: 1, lines removed: 1
```diff
@@ -255,5 +255,5 @@
# Submit all tasks
future_to_file = {
- executor.submit(compress_png, str(f), str(get_output_path(f, input_dir, output_dir))): f
+ executor.submit(compress_png, str(f), str(get_output_path(f, input_dir, output_dir)), force_bitdepth): f
for f in png_files
}
```
</details>
</tool-use>
---
<tool-use data-tool-type="generic" data-tool-name="read_lints">
<details>
<summary>Tool use: **read_lints** • Read lints for 1 file</summary>
Lint paths:
- `compress_pngs.py`
Lint results
**No lint errors found**
</details>
</tool-use>
---
---
Added command-line options to force 8-bit or 16-bit color depth. You can use:
- `--8bit` or `-8` to force 8-bit color depth
- `--16bit` or `-16` to force 16-bit color depth
If neither is specified, it auto-detects and preserves 16-bit when present.
Examples:
- `python compress_pngs.py` - Auto-detect (preserves 16-bit if present)
- `python compress_pngs.py --8bit` or `python compress_pngs.py -8` - Force all images to 8-bit
- `python compress_pngs.py --16bit` or `python compress_pngs.py -16` - Force all images to 16-bit
The script displays the selected mode at startup. The batch file will work with these options too.
---
_**User (2025-12-11 21:39Z)**_
Will this script work faster on a Threadripper 3970X than on an i9-13900kf?
---
_**Agent (model default, mode Agent)**_
---
Yes, it should run faster on the Threadripper 3970X, but not proportionally.
**Threadripper 3970X:**
- 32 physical cores, 64 threads
- The script uses `multiprocessing.cpu_count()`, which typically returns 64
- 64 worker processes can run in parallel
**i9-13900KF:**
- 8 P-cores + 8 E-cores = 16 physical cores, 24 threads
- The script would use 24 worker processes
**Expected performance:**
- Roughly 22.5x faster on the Threadripper, not 2.67x (64/24), because:
1. I/O bottlenecks: reading/writing files can limit scaling
2. Process overhead: more processes add overhead
3. PIL compression efficiency: may not scale linearly
4. Memory bandwidth: Threadrippers higher bandwidth helps, but may not be the main factor
**To optimize further:**
- Use an NVMe SSD for input/output
- Ensure sufficient RAM (the script loads images into memory)
- Consider adjusting worker count if I/O becomes a bottleneck
The Threadrippers higher core count should provide a clear speedup for this CPU-bound PNG compression workload.
---

View File

@@ -6,14 +6,21 @@ Compresses all PNG files in subdirectories with maximum parallelism.
import os import os
import sys import sys
import argparse
from pathlib import Path from pathlib import Path
from concurrent.futures import ProcessPoolExecutor, as_completed from concurrent.futures import ProcessPoolExecutor, as_completed
from PIL import Image from PIL import Image
import multiprocessing import multiprocessing
import time import time
def compress_png(input_path, output_path): def compress_png(input_path, output_path, force_bitdepth=None):
"""Compress a single PNG file.""" """Compress a single PNG file.
Args:
input_path: Path to input image
output_path: Path to output image
force_bitdepth: None (auto-detect), '8' (force 8-bit), or '16' (force 16-bit)
"""
try: try:
input_path = Path(input_path) input_path = Path(input_path)
output_path = Path(output_path) output_path = Path(output_path)
@@ -32,9 +39,13 @@ def compress_png(input_path, output_path):
original_size = os.path.getsize(input_path) original_size = os.path.getsize(input_path)
img = Image.open(input_path) img = Image.open(input_path)
# Check if image is 16-bit # Determine target bit depth
# PIL represents 16-bit grayscale as 'I' mode (32-bit integer) if force_bitdepth == '8':
# For color images, check pixel values to determine bit depth is_16bit = False
elif force_bitdepth == '16':
is_16bit = True
else:
# Auto-detect bit depth
is_16bit = False is_16bit = False
original_mode = img.mode original_mode = img.mode
@@ -58,16 +69,27 @@ def compress_png(input_path, output_path):
# If we can't determine, assume 8-bit to be safe # If we can't determine, assume 8-bit to be safe
pass pass
# Preserve 16-bit depth if present, otherwise convert as needed # Handle bit depth conversion based on target
if is_16bit: if is_16bit:
# For 16-bit images, preserve the mode # Force or preserve 16-bit
# PIL will save 16-bit when mode is 'I' (grayscale) or when using specific modes if force_bitdepth == '16':
# Force 16-bit: convert to appropriate 16-bit mode
if img.mode == 'I':
# Already 16-bit grayscale
pass
elif 'A' in img.mode or img.mode == 'LA':
# Convert to 16-bit RGBA (PIL limitation: may not fully preserve 16-bit color)
img = img.convert('RGBA')
else:
# Convert to 16-bit RGB
img = img.convert('RGB')
else:
# Preserve existing 16-bit
if img.mode == 'I': if img.mode == 'I':
# Keep 16-bit grayscale # Keep 16-bit grayscale
pass pass
elif img.mode in ('RGB', 'RGBA'): elif img.mode in ('RGB', 'RGBA'):
# Keep color mode - PIL may preserve 16-bit depending on how it was loaded # Keep color mode - PIL may preserve 16-bit depending on how it was loaded
# Note: PIL's PNG save may convert 16-bit RGB to 8-bit, but we preserve the mode
pass pass
else: else:
# Convert other modes while trying to preserve bit depth # Convert other modes while trying to preserve bit depth
@@ -75,6 +97,25 @@ def compress_png(input_path, output_path):
img = img.convert('RGBA') img = img.convert('RGBA')
else: else:
img = img.convert('RGB') img = img.convert('RGB')
else:
# Force or use 8-bit
if force_bitdepth == '8':
# Force 8-bit: ensure we're in 8-bit mode
if img.mode == 'I':
# Convert 16-bit grayscale to 8-bit
img = img.convert('L')
elif img.mode == 'RGBA':
# Keep RGBA for transparency support (8-bit)
img = img.convert('RGBA')
elif img.mode == 'RGB':
# Already 8-bit RGB
pass
else:
# Convert to 8-bit RGB
if 'A' in img.mode:
img = img.convert('RGBA')
else:
img = img.convert('RGB')
else: else:
# 8-bit images: convert as needed # 8-bit images: convert as needed
if img.mode == 'RGBA': if img.mode == 'RGBA':
@@ -139,6 +180,38 @@ def format_time(seconds):
return f"{hours:02d}:{minutes:02d}:{secs:02d}:{centiseconds:02d}" return f"{hours:02d}:{minutes:02d}:{secs:02d}:{centiseconds:02d}"
def main(): def main():
# Parse command-line arguments
parser = argparse.ArgumentParser(
description='Multithreaded PNG compression script with maximum parallelism.'
)
parser.add_argument(
'--8bit', '-8',
action='store_true',
dest='force_8bit',
help='Force 8-bit color depth for all images'
)
parser.add_argument(
'--16bit', '-16',
action='store_true',
dest='force_16bit',
help='Force 16-bit color depth for all images'
)
args = parser.parse_args()
# Determine bit depth setting
if args.force_8bit and args.force_16bit:
print("Error: Cannot specify both --8bit and --16bit. Choose one.")
return
elif args.force_8bit:
force_bitdepth = '8'
print("Mode: Forcing 8-bit color depth")
elif args.force_16bit:
force_bitdepth = '16'
print("Mode: Forcing 16-bit color depth")
else:
force_bitdepth = None
print("Mode: Auto-detect bit depth (preserve 16-bit if present)")
input_dir = Path('input') input_dir = Path('input')
output_dir = Path('output') output_dir = Path('output')
@@ -181,7 +254,7 @@ def main():
with ProcessPoolExecutor(max_workers=max_workers) as executor: with ProcessPoolExecutor(max_workers=max_workers) as executor:
# Submit all tasks # Submit all tasks
future_to_file = { future_to_file = {
executor.submit(compress_png, str(f), str(get_output_path(f, input_dir, output_dir))): f executor.submit(compress_png, str(f), str(get_output_path(f, input_dir, output_dir)), force_bitdepth): f
for f in png_files for f in png_files
} }