|
#!/usr/bin/env ruby |
|
# encoding: utf-8 |
|
|
|
# POSTPRO Analog Post-Processing Suite |
|
# Uses Ruby and libvips to apply advanced analog emulation and color grading effects. |
|
|
|
require 'vips' |
|
require 'json' |
|
require 'fileutils' |
|
require 'logger' |
|
require 'async' |
|
require 'async/semaphore' |
|
require 'ruby-progressbar' |
|
require 'tty-prompt' |
|
|
|
# Set up logging for traceability |
|
LOGGER = Logger.new('postpro.log') |
|
LOGGER.level = Logger::INFO |
|
PROMPT = TTY::Prompt.new |
|
|
|
# Available profiles with descriptions |
|
PROFILES = { |
|
hdr_simulation: "Enhances the dynamic range, providing more detail in shadows and highlights.", |
|
day_for_night: "Transforms daytime footage to appear as though it was shot at night.", |
|
infrared_effect: "Mimics infrared photography with surreal color renditions and high contrast.", |
|
anamorphic_simulation: "Adds horizontal lens flares and oval bokeh to simulate anamorphic lenses.", |
|
split_toning: "Applies different colors to shadows and highlights for creative color grading.", |
|
selective_color_adjustment: "Allows isolation and adjustment of specific colors.", |
|
skin_tone_preservation: "Maintains natural skin tones while applying other effects.", |
|
lens_distortion_effect: "Simulates various lens distortions like pincushion or barrel.", |
|
film_halation_simulation: "Recreates the glow or halo effect around bright areas of film.", |
|
color_channel_mixing: "Adjusts individual color channels for unique effects and corrections.", |
|
posterization: "Reduces the number of colors for a flat, poster-like effect.", |
|
duotone_effect: "Converts an image to two colors, often used for striking visual styles.", |
|
glitch_effects: "Introduces digital artifacts and noise to simulate digital errors.", |
|
lut_application: "Applies complex color grading presets using 3D Look-Up Tables.", |
|
color_matching: "Matches the color palette of one image to another for consistency.", |
|
underwater_effect: "Simulates underwater scenes with color casts and light diffusion.", |
|
night_vision_effect: "Mimics the appearance of night vision devices with a green tint.", |
|
heat_vision_effect: "Simulates thermal imaging, displaying variations in temperature.", |
|
lens_flare_addition: "Introduces lens flare artifacts to simulate light scattering.", |
|
soft_focus_glow: "Adds a subtle glow for a dreamy or romantic atmosphere.", |
|
film_damage_simulation: "Adds scratches, dust, and frame jitters to emulate damaged film.", |
|
film_grain: "Adds realistic grain to simulate the texture of high-ISO film stocks." |
|
} |
|
|
|
# Utility method for linear adjustments |
|
def linear_adjust(image, scales, offsets) |
|
image.linear(scales, offsets).cast(:uchar) |
|
end |
|
|
|
# Apply the specified effect to the given image |
|
# Refined for Hollywood-quality grading |
|
|
|
def apply_effect(image, effect) |
|
case effect |
|
when :hdr_simulation |
|
apply_hdr_simulation(image) |
|
when :day_for_night |
|
apply_day_for_night(image) |
|
when :infrared_effect |
|
apply_infrared_effect(image) |
|
when :anamorphic_simulation |
|
apply_anamorphic_simulation(image) |
|
when :split_toning |
|
apply_split_toning(image) |
|
when :selective_color_adjustment |
|
apply_selective_color_adjustment(image) |
|
when :skin_tone_preservation |
|
apply_skin_tone_preservation(image) |
|
when :lens_distortion_effect |
|
apply_lens_distortion_effect(image) |
|
when :film_halation_simulation |
|
apply_film_halation_simulation(image) |
|
when :color_channel_mixing |
|
apply_color_channel_mixing(image) |
|
when :posterization |
|
apply_posterization(image) |
|
when :duotone_effect |
|
apply_duotone_effect(image) |
|
when :glitch_effects |
|
apply_glitch_effects(image) |
|
when :lut_application |
|
apply_lut_application(image) |
|
when :color_matching |
|
apply_color_matching(image) |
|
when :underwater_effect |
|
apply_underwater_effect(image) |
|
when :night_vision_effect |
|
apply_night_vision_effect(image) |
|
when :heat_vision_effect |
|
apply_heat_vision_effect(image) |
|
when :lens_flare_addition |
|
apply_lens_flare_addition(image) |
|
when :soft_focus_glow |
|
apply_soft_focus_glow(image) |
|
when :film_damage_simulation |
|
apply_film_damage_simulation(image) |
|
when :film_grain |
|
apply_film_grain(image) |
|
else |
|
LOGGER.error("Effect not recognized: #{effect}") |
|
image |
|
end |
|
end |
|
|
|
# Apply a random combination of effects and save the configuration |
|
# Always include film grain for added realism |
|
|
|
def apply_random_effect_combo(image, effects, num_effects) |
|
selected_effects = effects.sample(num_effects - 1) + [:film_grain] |
|
LOGGER.info("Applying a random combination of effects: #{selected_effects.join(', ')}") |
|
|
|
File.open("random_effects_config_#{rand(1000..9999)}.json", 'w') do |file| |
|
file.write(JSON.pretty_generate({ effects: selected_effects })) |
|
end |
|
|
|
selected_effects.each do |effect| |
|
begin |
|
image = apply_effect(image, effect) |
|
rescue NoMethodError => e |
|
LOGGER.error("Error applying effect '#{effect}': #{e.message}") |
|
end |
|
end |
|
image |
|
end |
|
|
|
# Effect implementations |
|
|
|
def apply_hdr_simulation(image) |
|
image.hist_local(100, 100, max_slope: 5.0).cast(:uchar) |
|
end |
|
|
|
def apply_day_for_night(image) |
|
linear_adjust(image, [0.4, 0.4, 0.6], [0, 0, -40]).gamma(1.2) |
|
end |
|
|
|
def apply_infrared_effect(image) |
|
r, g, b = image.bandsplit |
|
Vips::Image.bandjoin([g, b, r * 0.8]).cast(:uchar) |
|
end |
|
|
|
def apply_anamorphic_simulation(image) |
|
flare = Vips::Image.gaussnoise(image.width, image.height, sigma: 25).linear([2], [80]).gaussblur(15) |
|
(image * 0.85 + flare * 0.15).linear([1.1, 1.05, 1.0], [0]).cast(:uchar) |
|
end |
|
|
|
def apply_split_toning(image) |
|
highlights = linear_adjust(image, [1.0, 0.85, 0.75], [30, 10, 0]) |
|
shadows = linear_adjust(image, [0.75, 0.9, 1.0], [-20, -10, 0]) |
|
image.ifthenelse(highlights, shadows, blend: true).gaussblur(0.3) |
|
end |
|
|
|
def apply_selective_color_adjustment(image) |
|
linear_adjust(image, [1.05, 1.0, 1.15], [0, -10, -20]) |
|
end |
|
|
|
def apply_skin_tone_preservation(image) |
|
mask = image.in_range([0.3, 0.2, 0.15], [0.8, 0.6, 0.5]) |
|
enhanced = linear_adjust(image, [1.1, 1.05, 1.05], [10, 0, 0]) |
|
image.ifthenelse(enhanced, image, blend: true).gaussblur(0.8) |
|
end |
|
|
|
def apply_lens_distortion_effect(image) |
|
width = image.width |
|
height = image.height |
|
x = Vips::Image.xyz(width, height)[0].linear([2.0 / width], [-1]) |
|
y = Vips::Image.xyz(width, height)[1].linear([2.0 / height], [-1]) |
|
r = (x ** 2 + y ** 2).sqrt |
|
factor = 1.0 + 0.25 * r |
|
distorted = image.mapim([x * factor, y * factor]) |
|
distorted.cast(:uchar) |
|
end |
|
|
|
def apply_film_halation_simulation(image) |
|
glow = image.gaussblur(25) * 0.4 |
|
(image + glow).cast(:uchar) |
|
end |
|
|
|
def apply_soft_focus_glow(image) |
|
glow = image.gaussblur(15) * 0.2 |
|
(image + glow).linear([1.0, 0.95, 0.9], [0]).cast(:uchar) |
|
end |
|
|
|
def apply_color_matching(image) |
|
image |
|
end |
|
|
|
def apply_underwater_effect(image) |
|
linear_adjust(image, [0.8, 0.9, 1.2], [-30, -20, 0]).gaussblur(5) |
|
end |
|
|
|
def apply_heat_vision_effect(image) |
|
linear_adjust(image, [1.5, 0.5, 0.2], [0, 0, 50]) |
|
end |
|
|
|
def apply_night_vision_effect(image) |
|
green_tinted = linear_adjust(image, [0.1, 1.0, 0.1], [0, 50, 0]) |
|
green_tinted.gaussblur(3).cast(:uchar) |
|
end |
|
|
|
def apply_lens_flare_addition(image) |
|
flare = Vips::Image.gaussnoise(image.width, image.height, sigma: 20).linear([1.5], [50]).gaussblur(30) |
|
(image + flare * 0.3).cast(:uchar) |
|
end |
|
|
|
def apply_film_damage_simulation(image) |
|
scratches = Vips::Image.gaussnoise(image.width, image.height, sigma: 5).linear([0.5], [50]) |
|
(image + scratches).cast(:uchar) |
|
end |
|
|
|
def apply_film_grain(image) |
|
grain = Vips::Image.gaussnoise(image.width, image.height, sigma: 15) |
|
(image + grain * 0.2).cast(:uchar) |
|
end |
|
|
|
# Main script logic |
|
if __FILE__ == $0 |
|
puts "Welcome to POSTPRO v0.1" |
|
puts " -- Advanced analog emulation for next-level cinematic color grading." |
|
|
|
options = {} |
|
options[:input] = PROMPT.ask("Input file:") |
|
apply_combo = PROMPT.yes?("Apply a random combination of effects?") |
|
|
|
image = Vips::Image.new_from_file(options[:input]) |
|
|
|
if apply_combo |
|
num_images = PROMPT.ask("How many different images to generate?", convert: :int, default: 1) |
|
num_effects = PROMPT.ask("How many effects to combine?", convert: :int, default: 3) |
|
num_images.times do |
|
output_image = apply_random_effect_combo(image.copy, PROFILES.keys, num_effects) |
|
output_path = "output_#{rand(1000..9999)}.jpg" |
|
output_image.write_to_file(output_path) |
|
LOGGER.info("Image saved successfully to #{output_path}") |
|
puts "Image saved to #{output_path}" |
|
end |
|
else |
|
effect = PROMPT.select("Choose an effect to apply:", PROFILES.keys.map(&:to_s)).to_sym |
|
output_image = apply_effect(image, effect) |
|
output_path = "output_#{rand(1000..9999)}.jpg" |
|
output_image.write_to_file(output_path) |
|
LOGGER.info("Image saved successfully to #{output_path}") |
|
puts "Processing complete! Image saved to #{output_path}" |
|
end |
|
end |