From e69b55b0c7469ebe0a95f00a78e001e2c5946765 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 10 Apr 2025 10:29:53 -0700 Subject: [PATCH 1/5] feat: Stock composition diagram stub --- src/stock_index_composition.jl | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/stock_index_composition.jl diff --git a/src/stock_index_composition.jl b/src/stock_index_composition.jl new file mode 100644 index 0000000..832618f --- /dev/null +++ b/src/stock_index_composition.jl @@ -0,0 +1,52 @@ +using Base +import Luxor as L + +@kwdef struct Component + pos::Tuple{UInt, UInt} + size::Tuple{UInt, UInt} + name::String + diff::Real +end +@kwdef struct CompositionDiagram + components::Array{Component} +end + +""" +Draws stock index composition chart +""" +function draw(diagram::CompositionDiagram, size=(800, 600)) + L.Drawing(size[1], size[2], :png) + L.background("black") + L.sethue("white") + for component in diagram.components + pos = L.Point(component.pos) + if component.diff > 0 + L.setcolor("green") + else + L.setcolor("red") + end + L.rect(pos, component.size[1], component.size[2], action = :fillstroke) + L.setcolor("white") + L.settext( + component.name, + L.Point(component.pos .+ component.size ./ 2), + halign="center", + valign="bottom", + ) + L.settext( + "$(component.diff)%", + L.Point(component.pos .+ component.size ./ 2), + halign="center", + valign="top", + ) + end + L.finish() +end + +diagram = CompositionDiagram( + components=[ + Component(pos=(0,0), size=(200,200),name="AAPL", diff=0.5), + Component(pos=(0,200), size=(200,100),name="GOOG", diff=-0.3), + ], +) +draw(diagram) -- 2.44.1 From a0fe1e843ac22807e052e0ccf221b1ee98316ea7 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 10 Apr 2025 11:07:11 -0700 Subject: [PATCH 2/5] feat: Sector in composition diagram --- src/stock_index_composition.jl | 97 ++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/src/stock_index_composition.jl b/src/stock_index_composition.jl index 832618f..f27ce2a 100644 --- a/src/stock_index_composition.jl +++ b/src/stock_index_composition.jl @@ -1,52 +1,103 @@ using Base import Luxor as L +""" +Colour of a component given its fluctuations +""" +function colour_of_diff(diff::Real)::Tuple{Real,Real,Real} + if diff > 0 + return (0.5,1,0.5) + else + return (1,0.5,0.5) + end +end + +component_text_size = 20 + @kwdef struct Component pos::Tuple{UInt, UInt} size::Tuple{UInt, UInt} name::String diff::Real end -@kwdef struct CompositionDiagram + +sector_title_font_size = 16 +sector_title_height = 20 + +@kwdef struct Sector + name::String + pos::Tuple{UInt, UInt} + size::Tuple{UInt, UInt} components::Array{Component} end +@kwdef struct CompositionDiagram + sectors::Array{Sector} +end + """ Draws stock index composition chart """ function draw(diagram::CompositionDiagram, size=(800, 600)) L.Drawing(size[1], size[2], :png) L.background("black") - L.sethue("white") - for component in diagram.components - pos = L.Point(component.pos) - if component.diff > 0 - L.setcolor("green") - else - L.setcolor("red") - end - L.rect(pos, component.size[1], component.size[2], action = :fillstroke) - L.setcolor("white") + for sector in diagram.sectors + L.sethue("gray") + L.rect(L.Point(sector.pos), sector.size[1], sector.size[2], action = :fillpreserve) + L.setline(2) + L.strokepath() + L.sethue("white") + L.setfont("Helvetica", sector_title_font_size) L.settext( - component.name, - L.Point(component.pos .+ component.size ./ 2), - halign="center", - valign="bottom", - ) - L.settext( - "$(component.diff)%", - L.Point(component.pos .+ component.size ./ 2), - halign="center", + sector.name, + L.Point(sector.pos), + halign="left", valign="top", ) + L.setfont("Helvetica", component_text_size) + for component in sector.components + pos = sector.pos .+ component.pos + L.setcolor(colour_of_diff(component.diff)) + L.rect(L.Point(pos), component.size[1], component.size[2], action = :fillpreserve) + L.sethue("white") + L.setline(5) + L.strokepath() + L.settext( + component.name, + L.Point(pos .+ component.size ./ 2), + halign="center", + valign="bottom", + ) + L.settext( + "$(component.diff)%", + L.Point(pos .+ component.size ./ 2), + halign="center", + valign="top", + ) + end end L.finish() end diagram = CompositionDiagram( - components=[ - Component(pos=(0,0), size=(200,200),name="AAPL", diff=0.5), - Component(pos=(0,200), size=(200,100),name="GOOG", diff=-0.3), + sectors=[ + Sector( + name="technology", + pos=(0,0), + size=(200,300), + components=[ + Component(pos=(0,20), size=(200,200),name="AAPL", diff=0.5), + Component(pos=(0,200), size=(200,100),name="GOOG", diff=-0.3), + ], + ), + Sector( + name="resources", + pos=(200,0), + size=(200,200), + components=[ + Component(pos=(0,20), size=(200,150),name="OIL", diff=0.1), + ], + ), ], ) draw(diagram) -- 2.44.1 From e468e60fc55b7f54b1d4b08fb009785dafc781b6 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 10 Apr 2025 15:23:06 -0700 Subject: [PATCH 3/5] feat: Temporal-dependent packing stub --- src/image.jl | 8 ++++++++ src/tdpacking.jl | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/image.jl create mode 100644 src/tdpacking.jl diff --git a/src/image.jl b/src/image.jl new file mode 100644 index 0000000..a34f334 --- /dev/null +++ b/src/image.jl @@ -0,0 +1,8 @@ +using Images + +""" +Converts image to grayscale +""" +function greyscale(c::Images.RGB)::UInt8 + return c.r +end diff --git a/src/tdpacking.jl b/src/tdpacking.jl new file mode 100644 index 0000000..a39c8d4 --- /dev/null +++ b/src/tdpacking.jl @@ -0,0 +1,39 @@ +# Time-dependent packing +struct Rect + pos::Tuple{UInt,UInt} + size::Tuple{UInt,UInt} + color::Real +end + +""" + divide_image(img::BitMatrix, size_min::UInt=16, size_max::UInt=256) + +Divides an image into rectangles of the same colour. The rectangles are not +allowed to have extreme aspect ratios. There is a minimal size of each retangle. +""" +function divide_image(img::Matrix{}, size_min::UInt=16, size_max::UInt=256)::Array{Rect} + result = Rect[] + + # Remaining area + area = sizeof(img) + + # Loop until the entire field is filled + while area > 0 + pos = (9, 9) + size = (threshold, threshold) + region = img.size + + # Try to expand this rectangle to the maximum size + + while size[1] < region[1] || size[2] < region[2] + end + + color = 0.5 + push!(result, Rect(pos, size, color)) + + # Update the boundary markers for the next iteration + area -= size[1] * size[2] + end + + return result +end -- 2.44.1 From 6d2df0e84f7f5a0b910234590243678d0637a151 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 11 Apr 2025 15:53:50 -0700 Subject: [PATCH 4/5] feat: Iterative rectangle refinement --- Manifest.toml | 2 +- Project.toml | 2 ++ src/image.jl | 4 ++-- src/tdpacking.jl | 62 ++++++++++++++++++++++++++++++++++-------------- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/Manifest.toml b/Manifest.toml index ae3e78a..7b38006 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.11.4" manifest_format = "2.0" -project_hash = "0a18a0873213e0e8660a5eb202a1fa9f1621a652" +project_hash = "ec502e8d73a739a6239b751344af945fa59e7806" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] diff --git a/Project.toml b/Project.toml index 9661941..a619579 100644 --- a/Project.toml +++ b/Project.toml @@ -7,10 +7,12 @@ version = "0.1.0" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" VideoIO = "d6d074c3-1acf-5d4c-9a43-ef38773959a2" [compat] FileIO = "1.17.0" Images = "0.26.2" Luxor = "4.2.0" +Statistics = "1.11.1" VideoIO = "1.1.1" diff --git a/src/image.jl b/src/image.jl index a34f334..3eaf2f9 100644 --- a/src/image.jl +++ b/src/image.jl @@ -3,6 +3,6 @@ using Images """ Converts image to grayscale """ -function greyscale(c::Images.RGB)::UInt8 - return c.r +function greyscale(c::Images.RGB)::Real + return Real(c.r) end diff --git a/src/tdpacking.jl b/src/tdpacking.jl index a39c8d4..fa602e7 100644 --- a/src/tdpacking.jl +++ b/src/tdpacking.jl @@ -1,38 +1,64 @@ +using Base +using Statistics, Images + # Time-dependent packing -struct Rect - pos::Tuple{UInt,UInt} - size::Tuple{UInt,UInt} +@kwdef struct Rect + pos::Tuple{Int,Int} + size::Tuple{Int,Int} color::Real end +function area(r::Rect)::Int + return r.size[1] * r.size[2] +end +function aspect_ratio(r::Rect)::Real + return r.size[1] / r.size[2] +end + """ - divide_image(img::BitMatrix, size_min::UInt=16, size_max::UInt=256) + divide_image(img::BitMatrix, size_min::Int=16, size_max::Int=256) Divides an image into rectangles of the same colour. The rectangles are not allowed to have extreme aspect ratios. There is a minimal size of each retangle. """ -function divide_image(img::Matrix{}, size_min::UInt=16, size_max::UInt=256)::Array{Rect} +function divide_image(img::Matrix{}, size_min::Int=16, size_max::Int=256, threshold_aspect::Real=3.0, threshold_std::Real=0.4)::Array{Rect} + remainder = Rect[Rect(pos=(1,1), size=size(img), color=mean(img))] result = Rect[] - # Remaining area - area = sizeof(img) - # Loop until the entire field is filled - while area > 0 - pos = (9, 9) - size = (threshold, threshold) - region = img.size + while length(remainder) > 0 + section = pop!(remainder) + + width = size_min + height = size_min # Try to expand this rectangle to the maximum size - while size[1] < region[1] || size[2] < region[2] + while width < section.size[1] || height < section.size[2] + subimg = img[section.pos[1]:section.pos[1]+width-1, section.pos[2]:section.pos[2]+height-1] + if std(subimg) >= threshold_std + break + end + colour = mean(subimg) + ar = width / height + + if width < section.size[1] && ar < threshold_aspect + width += 1 + elseif height < section.size[1] + height += 1 + else + break + end end + subimg = img[section.pos[1]:section.pos[1]+width-1, section.pos[2]:section.pos[2]+height-1] + rect = Rect( + pos=section.pos, + size=(width, height), + color=mean(subimg), + ) + push!(result, rect) - color = 0.5 - push!(result, Rect(pos, size, color)) - - # Update the boundary markers for the next iteration - area -= size[1] * size[2] + # FIXME: Add next iteration region end return result -- 2.44.1 From a189981d4eaa90d29d9daf78f7d953a35392554f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 14 Apr 2025 13:12:45 -0700 Subject: [PATCH 5/5] feat: Display divided image in greyscale --- src/tdpacking.jl | 89 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 11 deletions(-) diff --git a/src/tdpacking.jl b/src/tdpacking.jl index fa602e7..bb5959a 100644 --- a/src/tdpacking.jl +++ b/src/tdpacking.jl @@ -1,5 +1,14 @@ using Base using Statistics, Images +import Luxor as L + +@kwdef struct Region + pos::Tuple{Int,Int} + size::Tuple{Int,Int} + # If true, this region increases from x to y. This direction variable steers + # the direction of cut. + dir::Bool +end # Time-dependent packing @kwdef struct Rect @@ -16,41 +25,53 @@ function aspect_ratio(r::Rect)::Real end """ - divide_image(img::BitMatrix, size_min::Int=16, size_max::Int=256) + divide_image(img::Matrix{}, size_min::Int=16, size_max::Int=256) Divides an image into rectangles of the same colour. The rectangles are not allowed to have extreme aspect ratios. There is a minimal size of each retangle. """ -function divide_image(img::Matrix{}, size_min::Int=16, size_max::Int=256, threshold_aspect::Real=3.0, threshold_std::Real=0.4)::Array{Rect} - remainder = Rect[Rect(pos=(1,1), size=size(img), color=mean(img))] +function divide_image(img::Matrix{}; size_min::Int=16, size_max::Int=128, threshold_aspect::Real=3.0, threshold_std::Real=0.4)::Array{Rect} + remainder = [Region(pos=(1,1), size=size(img), dir=false)] result = Rect[] # Loop until the entire field is filled while length(remainder) > 0 section = pop!(remainder) - width = size_min - height = size_min + (px, py) = section.pos + (sx, sy) = section.size + + @assert px + sx - 1 <= size(img)[1] + @assert py + sy - 1 <= size(img)[2] + + width = 1 + height = 1 + + width_max = min(sx, size_max) + height_max = min(sy, size_max) # Try to expand this rectangle to the maximum size - while width < section.size[1] || height < section.size[2] - subimg = img[section.pos[1]:section.pos[1]+width-1, section.pos[2]:section.pos[2]+height-1] + while width < width_max || height < height_max + subimg = img[px:px+width-1, py:py+height-1] if std(subimg) >= threshold_std break end colour = mean(subimg) ar = width / height - if width < section.size[1] && ar < threshold_aspect + if width < width_max && ar < threshold_aspect width += 1 - elseif height < section.size[1] + elseif height < height_max && ar > 1/threshold_aspect height += 1 else break end end - subimg = img[section.pos[1]:section.pos[1]+width-1, section.pos[2]:section.pos[2]+height-1] + @assert width <= sx + @assert height <= sy + + subimg = img[px:px+width-1, py:py+height-1] rect = Rect( pos=section.pos, size=(width, height), @@ -58,8 +79,54 @@ function divide_image(img::Matrix{}, size_min::Int=16, size_max::Int=256, thresh ) push!(result, rect) - # FIXME: Add next iteration region + if section.dir + if width < sx + push!(remainder, Region( + pos=(px + width, py), + size=(sx - width, height), + dir=false, + )) + end + if height < sy + push!(remainder, Region( + pos=(px, py + height), + size=(sx, sy - height), + dir=true, + )) + end + else + if height < sy + push!(remainder, Region( + pos=(px, py + height), + size=(width, sy - height), + dir=true, + )) + end + if width < sx + push!(remainder, Region( + pos=(px + width, py), + size=(sx - width, sy), + dir=false, + )) + end + end end return result end + +""" + draw_rect(size::Tuple{Int, Int}, rects::Array{Rect}) + +Displays the divided image in greyscale. +""" +function draw_rect(size::Tuple{Int, Int}, rects::Array{Rect}) + L.Drawing(size[1], size[2], :png) + L.background("black") + for rect in rects + c = N0f8(rect.color) + setcolor(c, c, c) + L.rect(L.Point(rect.pos), rect.size[1], rect.size[2], action = :fill) + end + L.finish() +end -- 2.44.1