bad_apple.exs (3525B)
1 defmodule BadApple.FrameCtx do 2 use Agent 3 4 def start_link(_opts \\ []) do 5 Agent.start_link(fn -> [] end, name: __MODULE__) 6 end 7 8 def set_target(bits), do: Agent.update(__MODULE__, fn _ -> bits end) 9 def get_target(), do: Agent.get(__MODULE__, & &1) 10 end 11 12 defmodule BadApple do 13 alias Types.Chromosome 14 @behaviour Problem 15 16 @width 48 17 @height 36 18 @size @width * @height 19 20 @impl true 21 def genotype do 22 genes = 23 Stream.repeatedly(fn -> Enum.random([0, 1]) end) 24 |> Enum.take(@size) 25 26 %Chromosome{genes: genes, size: @size} 27 end 28 29 @impl true 30 def fitness_function(%Chromosome{genes: genes}) do 31 target = BadApple.FrameCtx.get_target() 32 33 matches = 34 Enum.zip(genes, target) 35 |> Enum.count(fn {a, b} -> a == b end) 36 37 matches / @size 38 end 39 40 @impl true 41 def terminate?(population, generation) do 42 best = hd(population) 43 best.fitness >= 0.985 or generation >= 120 44 end 45 end 46 47 defmodule BadApple.FrameIO do 48 def load_bits(path) do 49 {:ok, image} = Vix.Vips.Image.new_from_file(path) 50 width = Vix.Vips.Image.width(image) 51 height = Vix.Vips.Image.height(image) 52 {:ok, bin} = Vix.Vips.Image.write_to_binary(image) 53 54 expected = width * height 55 56 if byte_size(bin) != expected do 57 raise "Expected #{expected} bytes, got #{byte_size(bin)} for #{path}" 58 end 59 60 for <<px <- bin>> do 61 if px >= 128, do: 1, else: 0 62 end 63 end 64 end 65 66 defmodule BadApple.Output do 67 @width 48 68 @height 36 69 70 def save_bits_as_png(bits, out_path) do 71 bytes = 72 bits 73 |> Enum.map(fn b -> if b == 1, do: 255, else: 0 end) 74 |> :erlang.list_to_binary() 75 76 {:ok, image} = 77 Vix.Vips.Image.new_from_binary(bytes, @width, @height, 1, :VIPS_FORMAT_UCHAR) 78 79 :ok = File.mkdir_p!(Path.dirname(out_path)) 80 :ok = Vix.Vips.Image.write_to_file(image, out_path) 81 end 82 end 83 84 {:ok, _} = BadApple.FrameCtx.start_link() 85 start_frame = 501 86 count = 200 87 88 frame_paths = 89 "priv/frames_target/*.png" 90 |> Path.wildcard() 91 |> Enum.sort() 92 |> Enum.drop(start_frame - 1) 93 |> Enum.take(count) 94 95 IO.puts("Processing #{length(frame_paths)} frames...") 96 97 last_best = nil 98 99 for {frame_path, idx} <- Enum.with_index(frame_paths, 1), reduce: last_best do 100 prev_best -> 101 target_bits = BadApple.FrameIO.load_bits(frame_path) 102 BadApple.FrameCtx.set_target(target_bits) 103 104 init_type = 105 if prev_best do 106 fn genotype, opts -> 107 population_size = Keyword.get(opts, :population_size, 60) 108 seed = prev_best.genes 109 110 seeded = 111 Enum.map(1..population_size, fn _ -> 112 genes = 113 Enum.map(seed, fn bit -> 114 if :rand.uniform() < 0.10, do: 1 - bit, else: bit 115 end) 116 117 %Types.Chromosome{genes: genes, size: length(genes)} 118 end) 119 120 [genotype.() | tl(seeded)] 121 end 122 else 123 &Toolbox.Initialize.random/2 124 end 125 126 {best, generation} = 127 Genetic.run(BadApple, 128 population_size: 100, 129 initialization_type: init_type, 130 selection_type: &Toolbox.Selection.elite/2, 131 mutation_type: &Toolbox.Mutation.flip/2, 132 mutation_rate: 0.010, 133 crossover_type: &Toolbox.Crossover.uniform/3 134 ) 135 136 out_path = "priv/frames_out/#{String.pad_leading(Integer.to_string(idx), 5, "0")}.png" 137 BadApple.Output.save_bits_as_png(best.genes, out_path) 138 139 IO.puts( 140 "\nFrame #{idx} done | gen=#{generation} | fitness=#{Float.round(best.fitness, 4)} | #{Path.basename(frame_path)}" 141 ) 142 143 best 144 end