Amplitude Modulated (AM) Digital Halftoning
Key Steps
-
Generate a threshold array
- Typically small, such as 8×8 , 12×12 , or 16×16
- Contains arranged threshold values used to modulate dot growth
-
Tile the threshold array over the image
- Repeat it across the entire image so each input pixel is matched to a threshold
- The array acts like a stencil for comparing tones
-
Compare each image pixel to the matching threshold value
- For each pixel:
Output={255if Intensity>Threshold0otherwise \text{Output} = \begin{cases} 255 & \text{if } \text{Intensity} > \text{Threshold} \\ 0 & \text{otherwise} \end{cases} Output={2550if Intensity>Thresholdotherwise - This produces a binary result per pixel: "on" (255) or "off" (0)
- For each pixel:
-
Resulting pixels form clustered dots
- Dot size grows with tone intensity
- Dark tones → more "on" pixels in a tile; light tones → fewer
Note:
In AM halftoning , threshold values are usually arranged to produce round, square, or elliptical dot shapes and are reused across the image to maintain a regular grid structure.
detail
Steps to Build a Threshold Matrix
It's a small grid (like 8×8 or 16×16) containing values from 0 to 255 that define the dot shape and growth order. When tiled across an image, it tells each pixel when to turn black based on its tone.
🔹 1. Choose Cell Size & Screen Parameters
- Pick cell size (e.g., 8×8 pixels)
- Select screen frequency (dots/inch)
- Choose a screen angle (dot orientation)
🔹 2. Assign Normalized Coordinates
Inside the cell:
-
Each pixel gets (x, y) ranging from −1 to +1
-
You rotate the coordinates to match the screen angle:
mathx′ = x * cos(θ) − y * sin(θ) y′ = x * sin(θ) + y * cos(θ)
🔹 3. Apply Spot Function
Choose a cost function to shape the dot---examples:
- Circle: x′2+y′2x'^2 + y'^2x′2+y′2
- Line: x′x'x′
- Square : max(x′,y′)\max(x', y')max(x′,y′)
- Diamond: ∣x∣+∣y′∣|x|+ |y'|∣x∣+∣y′∣
This gives each pixel a cost, which defines when it should turn black as tone increases.
🔹 4. Histogram Equalization
- Sort the cost values from low to high
- Map them linearly to threshold values from 0--255
- Ties (same cost) can be broken by angle to center or manually
🔹 5. Assemble the Matrix
You now have a full threshold matrix that:
- Encodes dot shape
- Responds smoothly to tone
- Can be tiled across any grayscale image for halftoning
Would you like help building one programmatically or testing how different spot functions affect the pattern? I can simulate a couple matrix styles to show their real-world effect!
AM Halftoning Scenario Setup
Given a grayscale image: 300×300 pixels , and a printer with 600 DPI resolution, we want to convert the image into a halftone while:
- Maintaining physical print size
- Adapting to printer grid
- Computing halftone cell size via screen frequency
Step 1: Image DPI and Physical Size
If the image is created at 300 DPI:
-
Physical size = \\frac{300 \\text{ pixels}}{300 \\text{ DPI}} = 1 \\text{ inch}
-
The printer uses 600 DPI , so to preserve the same printed size, we need to resample the image to:
Resampled size=Original Pixel Width×Printer DPIImage DPI=300×600300=600 pixels \text{Resampled size} = \text{Original Pixel Width} × \frac{\text{Printer DPI}}{\text{Image DPI}} = 300 × \frac{600}{300} = 600 \text{ pixels} Resampled size=Original Pixel Width×Image DPIPrinter DPI=300×300600=600 pixels
Final resampled image: 600 × 600 pixels
Step 2: Screen Frequency and Cell Size
Screen frequency controls how many dot clusters (halftone cells) are printed per inch.
Let's say:
- Screen frequency (LPI) = 60 lines/inch
- Printer resolution (DPI) = 600 dots/inch
Then the cell size in pixels is:
Halftone Cell Size=Printer DPIScreen Frequency=60060=10 pixels \text{Halftone Cell Size} = \frac{\text{Printer DPI}}{\text{Screen Frequency}} = \frac{600}{60} = 10 \text{ pixels} Halftone Cell Size=Screen FrequencyPrinter DPI=60600=10 pixels
So each halftone cell covers:
- 10 × 10 device pixels
The final image will have:
- 60010=60 halftone cells per row or column \frac{600}{10} = 60 \text{ halftone cells per row or column} 10600=60 halftone cells per row or column
Resulting in 60 × 60 dots forming the halftone pattern across the printed image
📌 Summary Table
Image DPI | Printer DPI | Screen Frequency (LPI) | Resampled Size | Cell Size (pixels) | Dots Across Image |
---|---|---|---|---|---|
300 | 600 | 60 | 600 × 600 px | 10 × 10 px | 60 × 60 |
🛠️ Step-by-Step Guide (with Code)
⚙️ 1. Resample Image to Match DPI
Let's assume your screen frequency is 60 lines/inch → each halftone cell is:
python
cell_size = int(600 / 60) # = 10 pixels per cell
Now, convert your image to match printer resolution (600×600):
python
import cv2
image = cv2.imread("input_image.jpg", cv2.IMREAD_GRAYSCALE)
image_resampled = cv2.resize(image, (600, 600), interpolation=cv2.INTER_CUBIC)
🧩 2. Define Spot Function (e.g., Circular)
python
def spot_function(x, y):
return x**2 + y**2
🔄 3. Tile Image & Apply Halftoning
python
import numpy as np
def am_halftone(image, cell_size, angle_degrees):
angle = np.deg2rad(angle_degrees)
height, width = image.shape
output = np.zeros_like(image)
for i in range(0, height, cell_size):
for j in range(0, width, cell_size):
tile = image[i:i+cell_size, j:j+cell_size]
cost_map = []
for u in range(tile.shape[0]):
for v in range(tile.shape[1]):
# Normalize coords
x = 2 * (u + 0.5)/cell_size - 1
y = 2 * (v + 0.5)/cell_size - 1
# Rotate
x_r = x * np.cos(angle) - y * np.sin(angle)
y_r = x * np.sin(angle) + y * np.cos(angle)
cost = spot_function(x_r, y_r)
cost_map.append((u, v, cost))
# Histogram equalization
cost_map.sort(key=lambda x: x[2])
thresholds = np.linspace(0, 255, len(cost_map))
for idx, (u, v, _) in enumerate(cost_map):
thresh = thresholds[idx]
output[i+u, j+v] = 0 if tile[u, v] > thresh else 255
return output
📤 4. Run the Conversion
python
halftone_image = am_halftone(image_resampled, cell_size=10, angle_degrees=45)
cv2.imwrite("halftone_output.png", halftone_image)
🔍 Key Insight
Resampling makes your image match the printer's pixel grid, so that each halftone cell aligns with real device pixels. That's critical---otherwise your dot shapes will be distorted or irregular.
Would you like to experiment with elliptical spot functions or visualize how dot shapes evolve when you change resolution or screen frequency? We can make a little halftoning playground together!
附录
符号说明
1. Physical size
-
Definition: The physical dimensions of an image when printed.
-
Units: Inches (in)
-
Formula:
Physical Width (in)=Pixel WidthDPI,Physical Height (in)=Pixel HeightDPI \text{Physical Width (in)} = \frac{\text{Pixel Width}}{\text{DPI}}, \quad \text{Physical Height (in)} = \frac{\text{Pixel Height}}{\text{DPI}} Physical Width (in)=DPIPixel Width,Physical Height (in)=DPIPixel Height
-
Example: An image with 3000×2400 pixels at 300 DPI has a physical size of 10×8 inches.
2. DPI (Dots Per Inch)
- Definition: How many dots a printer can place in one inch when printing.
- Used for: Output resolution (printing).
- Note: Often stored in image metadata (like TIFF, JPEG).
- Common values: 72 (screen), 300 (high quality print), 600+ (laser printer).
3. PPI (Pixels Per Inch)
-
Definition: The pixel density of a digital image or screen---how many pixels are packed into one inch.
-
Used for: Screen/display resolution.
-
Clarification:
-
In images, PPI and DPI are often used interchangeably, but:
- DPI is output-oriented (printing),
- PPI is input/display-oriented (screens, raster images).
-
4. Resolution
-
Definition: General term referring to the level of detail in an image.
-
Can mean:
- DPI (for physical output/print),
- PPI (for screen/display),
- Or pixel dimensions (width × height in pixels).
5. Pixel array size (pixel dimensions)
-
Definition: Number of pixels that make up the image.
-
Components:
- Pixel Width: number of columns
- Pixel Height: number of rows
-
Units: Pixels (px)
-
Example: 1920×1080 means 1920 columns and 1080 rows of pixels.
🔁 Relationships
If you know any two of:
- pixel dimensions,
- physical size,
- resolution (DPI or PPI),
...you can compute the third.
Example:
- Pixel size: 6000×4000
- DPI: 300
→ Physical size = 20×13.33 inches
Summary Table
Term | Definition | Unit | Applies To |
---|---|---|---|
Physical Size | Size when printed | Inches | |
DPI | Dots per inch (printer dots) | Dots/inch | Print device |
PPI | Pixels per inch (image/screen density) | Pixels/in | Screen/Image |
Resolution | Level of detail | DPI/PPI | Varies |
Pixel Size | Number of pixels in width/height | Pixels | Image data |
参考文献
https://engineering.purdue.edu/\~bouman/ece637/notes/
https://github.com/philgyford/python-halftone
介绍https://shankhya.github.io/musings/halftoning.pdf
https://github.com/tfuxu/dither-go/tree/977f24c2d937eaff404f15270f29977c0719f5f8
研究人
ReversibleHalftoning代码
https://github.com/MenghanXia/ReversibleHalftoning
艺术半调 https://github.com/setanarut/halftonism
源码
python
import numpy as np
import cv2
def spot_function(x, y, mode='circle'):
"""
Spot function for AM halftoning.
mode: 'circle', 'line', 'square', 'diamond'
"""
if mode == 'circle':
return x**2 + y**2
elif mode == 'line':
return x
elif mode == 'square':
return max(abs(x), abs(y))
elif mode == 'diamond':
return abs(x) + abs(y)
# 可扩展更多形状
else:
raise ValueError(f"Unknown spot function mode: {mode}")
def am_halftone(image, cell_size, angle_degrees, spot_mode='circle'):
angle = np.deg2rad(angle_degrees)
height, width = image.shape
output = np.zeros_like(image)
# 预先计算cost_map和thresholds
cost_map = []
for u in range(cell_size):
for v in range(cell_size):
x = 2 * (u + 0.5) / cell_size - 1
y = 2 * (v + 0.5) / cell_size - 1
x_r = x * np.cos(angle) - y * np.sin(angle)
y_r = x * np.sin(angle) + y * np.cos(angle)
cost = spot_function(x_r, y_r, mode=spot_mode)
cost_map.append((u, v, cost))
cost_map.sort(key=lambda x: x[2])
thresholds = np.linspace(0, 255, len(cost_map))
for i in range(0, height, cell_size):
for j in range(0, width, cell_size):
tile = image[i:i+cell_size, j:j+cell_size]
for idx, (u, v, _) in enumerate(cost_map):
if u < tile.shape[0] and v < tile.shape[1]:
thresh = thresholds[idx]
output[i+u, j+v] = 0 if tile[u, v] < thresh else 255
return output
def main():
import argparse
parser = argparse.ArgumentParser(description="AM Halftoning")
parser.add_argument("input_image", help="Path to input image (grayscale or RGB)")
parser.add_argument("--cell_size", type=int, default=8, help="Cell size for halftoning")
parser.add_argument("--angle", type=float, default=0, help="Screen angle for grayscale or Red channel")
parser.add_argument("--angle_g", type=float, default=15, help="Screen angle for Green channel (RGB only)")
parser.add_argument("--angle_b", type=float, default=30, help="Screen angle for Blue channel (RGB only)")
parser.add_argument("--angle_a", type=float, default=45, help="Screen angle for Blue channel (RGB only)")
parser.add_argument("--spot_mode", type=str, default="diamond", choices=["circle", "line", "square", "diamond"], help="Spot function mode")
parser.add_argument("--output", type=str, default="E:\\code\\python\\Halftoning\\image\\halftone_diamond.bmp", help="Output image file name")
args = parser.parse_args()
image = cv2.imread(args.input_image, cv2.IMREAD_UNCHANGED)
if image is None:
print(f"Failed to load image: {args.input_image}")
return
print("image shape:", image.shape)
if len(image.shape) == 2: # Grayscale
halftone_image = am_halftone(image, cell_size=args.cell_size, angle_degrees=args.angle, spot_mode=args.spot_mode)
elif len(image.shape) == 3 and image.shape[2] == 3: # RGB
angles = [args.angle, args.angle_g, args.angle_b]
channels = cv2.split(image)
halftoned_channels = []
for ch, ang in zip(channels, angles):
halftoned = am_halftone(ch, cell_size=args.cell_size, angle_degrees=ang, spot_mode=args.spot_mode)
halftoned_channels.append(halftoned)
halftone_image = cv2.merge(halftoned_channels)
elif len(image.shape) == 3 and image.shape[2] == 4: # RGBA (actually BGRA in OpenCV)
angles = [args.angle_b, args.angle_g, args.angle] # B, G, R
b, g, r, a = cv2.split(image)
bgra_channels = [b, g, r]
halftoned_bgr = []
for ch, ang in zip(bgra_channels, angles):
halftoned = am_halftone(ch, cell_size=args.cell_size, angle_degrees=ang, spot_mode=args.spot_mode)
halftoned_bgr.append(halftoned)
halftone_image = cv2.merge(halftoned_bgr + [a])
else:
print("Unsupported image format.")
return
cv2.imwrite(args.output, halftone_image)
print(f"Halftone image saved to {args.output}")
if __name__ == "__main__":
main()