diff --git a/Project.toml b/Project.toml index dac0219..3ee752a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,8 +1,9 @@ name = "ImageDraw" uuid = "4381153b-2b60-58ae-a1ba-fd683676385f" -version = "0.2.2" +version = "0.2.3" [deps] +Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/src/ImageDraw.jl b/src/ImageDraw.jl index acba1f5..822c4ac 100644 --- a/src/ImageDraw.jl +++ b/src/ImageDraw.jl @@ -1,7 +1,7 @@ module ImageDraw # package code goes here -using ImageCore, Distances +using ImageCore, Distances, Combinatorics include("core.jl") include("line2d.jl") @@ -9,11 +9,13 @@ include("ellipse2d.jl") include("circle2d.jl") include("paths.jl") include("cross.jl") +include("background.jl") #export methods export draw, draw!, + generatecanvas, bresenham, xiaolin_wu @@ -22,6 +24,10 @@ export #Drawable Drawable, + #backgrounds + SolidBackground, + StripedBackground, + #Point Point, diff --git a/src/background.jl b/src/background.jl new file mode 100644 index 0000000..e68db16 --- /dev/null +++ b/src/background.jl @@ -0,0 +1,64 @@ +""" + gen_img = generatecanvas(colortype, x, y) + gen_img = generatecanvas(colortype, x, y, background) + gen_img = generatecanvas(colortype, [x,y]) + gen_img = generatecanvas(colortype, [x,y], background) + + Creates an image of with the given colortype and size. + Defaults to a black solid image +""" +generatecanvas(colortype::Type, x::Int, y::Int) = zeros(colortype, (x,y)) + +generatecanvas(colortype::Type, size::NTuple{2, Int}) = zeros(colortype, size) + +generatecanvas(colortype::Type, x::Int, y::Int, b::AbstractBackground) = + generatecanvas(colortype, (x,y), b) + +function generatecanvas(colortype::Type, size::NTuple{2, Int}, b::AbstractBackground) + draw!(zeros(colortype, (size...)), b) +end + + +""" + new_img = draw!(img, solid_background) + + Paints the entire given image a solid color +""" +function draw!(img::AbstractArray{T,2}, b::SolidBackground) where {T<:Colorant} + b.color == zero(typeof(b.color)) ? (return img) : nothing + x, y = size(img) + for i = 1:y + for j = 1:x + draw!(img, Point(j, i), b.color) + end + end + img +end + +""" + new_img = draw!(img, striped_background) + + Layers colors onto a given image to set a "background" +""" +function draw!(img::AbstractArray{T,2}, b::StripedBackground) where {T<:Colorant} + x,y = size(img) + for (e,(c,d)) in enumerate(zip(b.colors,b.distances)) + draw!(img, LineNormal(d,b.θ), c) + for i = 1:y + cnt = 0 + for j = 1:x + if img[i,j] == zero(typeof(c)) + draw!(img, Point(j,i), c) + cnt += 1 + elseif img[i,j] == c + break + elseif img[i,j] in b.colors[1:e-1] + cnt = 1 + continue + end + end + cnt == 0 ? break : continue + end + end + img +end diff --git a/src/core.jl b/src/core.jl index 4fa1ff6..45a6ced 100644 --- a/src/core.jl +++ b/src/core.jl @@ -14,8 +14,39 @@ struct Point <: Drawable y::Int end -abstract type Line <: Drawable end -abstract type Circle <: Drawable end +abstract type AbstractPath <: Drawable end +abstract type AbstractLine <: Drawable end +abstract type AbstractShape <: Drawable end +abstract type AbstractBackground <: Drawable end + + +abstract type AbstractPolygon <: AbstractShape end +abstract type AbstractEllipse <: AbstractShape end +abstract type AbstractCircle <: AbstractEllipse end + +""" + background = SolidBackground(color) + +A `Drawable` background that will fill the 'background' of an image with +the set color +""" + +struct SolidBackground{T<:Colorant} <: AbstractBackground + color::T +end + +""" + background = StripedBackground(color) + +A `Drawable` background that will fill the 'background' of an image with +the given colors at the intervals given at the given angle +""" + +struct StripedBackground{T<:Colorant, U<:Real, V<:Real} <: AbstractBackground + colors::Vector{T} + distances::Vector{U} + θ::V +end """ @@ -24,7 +55,7 @@ abstract type Circle <: Drawable end A `Drawable` infinite length line passing through the two points `p1` and `p2`. """ -struct LineTwoPoints <: Line +struct LineTwoPoints <: AbstractLine p1::Point p2::Point end @@ -36,7 +67,7 @@ A `Drawable` infinte length line having perpendicular length `ρ` from origin and angle `θ` between the perpendicular and x-axis """ -struct LineNormal{T<:Real, U<:Real} <: Line +struct LineNormal{T<:Real, U<:Real} <: AbstractLine ρ::T θ::U end @@ -46,7 +77,7 @@ end A `Drawable` circle passing through points `p1`, `p2` and `p3` """ -struct CircleThreePoints <: Circle +struct CircleThreePoints <: AbstractCircle p1::Point p2::Point p3::Point @@ -57,7 +88,7 @@ end A `Drawable` circle having center `center` and radius `ρ` """ -struct CirclePointRadius{T<:Real} <: Circle +struct CirclePointRadius{T<:Real} <: AbstractCircle center::Point ρ::T end @@ -67,7 +98,7 @@ end A `Drawable` finite length line between `p1` and `p2` """ -struct LineSegment <: Drawable +struct LineSegment <: AbstractLine p1::Point p2::Point end @@ -80,7 +111,7 @@ of points in `[point]`. !!! note This will create a non-closed path. For a closed path, see `Polygon` """ -struct Path <: Drawable +struct Path <: AbstractPath vertices::Vector{Point} end @@ -90,7 +121,7 @@ end A `Drawable` ellipse with center `center` and parameters `ρx` and `ρy` """ -struct Ellipse{T<:Real, U<:Real} <: Drawable +struct Ellipse{T<:Real, U<:Real} <: AbstractEllipse center::Point ρx::T ρy::U @@ -104,7 +135,7 @@ consecutive points in `[vertex]` along with the first and last point. !!! note This will create a closed path. For a non-closed path, see `Path` """ -struct Polygon <: Drawable +struct Polygon <: AbstractPolygon vertices::Vector{Point} end @@ -120,7 +151,7 @@ A `Drawable` regular polygon. * `θ::Real` : orientation of the polygon w.r.t x-axis (in radians) """ -struct RegularPolygon{T<:Real, U<:Real} <: Drawable +struct RegularPolygon{T<:Real, U<:Real} <: AbstractPolygon center::Point side_count::Int side_length::T @@ -131,7 +162,7 @@ end cross = Cross(c, range::UnitRange{Int}) A `Drawable` cross passing through the point `c` with arms ranging across `range`. """ -struct Cross <: Drawable +struct Cross <: AbstractPath c::Point range::UnitRange{Int} end @@ -186,10 +217,9 @@ function draw!(img::AbstractArray{T,2}, point::Point, color::T) where T<:Coloran end """ - img_new = drawifinbounds!(img, y, x, color) - img_new = drawifinbounds!(img, Point, color) - img_new = drawifinbounds!(img, CartesianIndex, color) + img_new = drawifinbounds!(img, point, color) + img_new = drawifinbounds!(img, cartesianIndex, color) Draws a single point after checkbounds() for coordinate in the image. Color Defaults to oneunit(T) @@ -203,3 +233,32 @@ function drawifinbounds!(img::AbstractArray{T,2}, y::Int, x::Int, color::T) wher if checkbounds(Bool, img, y, x) img[y, x] = color end img end + +""" + img_new = drawwiththickness!(img, y, x, color, thickness) + img_new = drawwiththickness!(img, point, color, thickness) + img_new = drawwiththickness!(img, cartesianIndex, color, thickness) + +Draws pixel with given thickness +Color Defaults to oneunit(T) +Thickness defaults to 1 + +""" + +drawwiththickness!(img::AbstractArray{T,2}, p::Point, color::T = oneunit(T), thickness::Int=1) where {T<:Colorant} = drawwiththickness!(img, p.y, p.x, color, thickness) +drawwiththickness!(img::AbstractArray{T,2}, p::CartesianIndex{2}, color::T = oneunit(T), thickness::Int=1) where {T<:Colorant} = drawifinbounds!(img, Point(p), color, thickness) + +function drawwiththickness!(img::AbstractArray{T,2}, y0::Int, x0::Int, color::T, thickness::Int) where {T<:Colorant} + n = Int(round(thickness / 2)) + evn = thickness % 2 == 1 ? 0 : 1 + pixels = [i for i = -(n-evn):n] + + for (x,y) in Combinatorics.combinations(pixels, 2) + drawifinbounds!(img, y0+y, x0+x, color) + drawifinbounds!(img, y0+y, x0-x, color) + drawifinbounds!(img, y0-y, x0+x, color) + drawifinbounds!(img, y0-y, x0-x, color) + end + drawifinbounds!(img, y0, x0, color) + img +end diff --git a/src/cross.jl b/src/cross.jl index cbf070a..70b2489 100644 --- a/src/cross.jl +++ b/src/cross.jl @@ -4,12 +4,12 @@ A `Drawable` cross passing through the point `c` with arms that are `arm` pixels """ Cross(c, arm::Int) = Cross(c, -arm:arm) -function draw!(img::AbstractArray{T, 2}, cross::Cross, color::T) where T<:Colorant +function draw!(img::AbstractArray{T, 2}, cross::Cross, color::T, thickness::Int=1) where T<:Colorant for Δx in cross.range - drawifinbounds!(img, cross.c.y, cross.c.x + Δx, color) + drawwiththickness!(img, cross.c.y, cross.c.x + Δx, color, thickness) end for Δy in cross.range - drawifinbounds!(img, cross.c.y + Δy, cross.c.x, color) + drawwiththickness!(img, cross.c.y + Δy, cross.c.x, color, thickness) end img end diff --git a/src/line2d.jl b/src/line2d.jl index 5689e7e..8418a12 100644 --- a/src/line2d.jl +++ b/src/line2d.jl @@ -1,4 +1,3 @@ - #Function to return valid intersections of lines with image boundary function get_valid_intersections(intersections::Vector{Tuple{T,U}}, indsx::AbstractUnitRange, indsy::AbstractUnitRange) where {T<:Real, U<:Real} @@ -17,9 +16,12 @@ LineTwoPoints(x0::Int, y0::Int, x1::Int, y1::Int) = LineTwoPoints(Point(x0, y0), LineTwoPoints(p1::CartesianIndex{2}, p2::CartesianIndex{2}) = LineTwoPoints(Point(p1), Point(p2)) draw!(img::AbstractArray{T,2}, line::LineTwoPoints, method::Function = bresenham) where {T<:Colorant} = - draw!(img, line, oneunit(T), method) + draw!(img, line, oneunit(T), 1, method) + +draw!(img::AbstractArray{T,2}, line::LineTwoPoints, color::T, method::Function = bresenham) where {T<:Colorant} = + draw!(img, line, color, 1, method) -function draw!(img::AbstractArray{T,2}, line::LineTwoPoints, color::T, method::Function = bresenham) where T<:Colorant +function draw!(img::AbstractArray{T,2}, line::LineTwoPoints, color::T, thickness::Int, method::Function = bresenham) where T<:Colorant indsy, indsx = axes(img) x1 = line.p1.x; y1 = line.p1.y x2 = line.p2.x; y2 = line.p2.y @@ -28,7 +30,7 @@ function draw!(img::AbstractArray{T,2}, line::LineTwoPoints, color::T, method::F intersections_y = [(x1 + (y-y1)/m, y) for y in (first(indsy), last(indsy))] valid_intersections = get_valid_intersections(vcat(intersections_x, intersections_y), indsx, indsy) if length(valid_intersections) > 0 - method(img, round(Int,valid_intersections[1][2]), round(Int,valid_intersections[1][1]), round(Int,valid_intersections[2][2]), round(Int,valid_intersections[2][1]), color) + method(img, round(Int,valid_intersections[1][2]), round(Int,valid_intersections[1][1]), round(Int,valid_intersections[2][2]), round(Int,valid_intersections[2][1]), color, thickness) else img end @@ -40,9 +42,9 @@ end LineNormal(τ::Tuple{T,U}) where {T<:Real, U<:Real} = LineNormal(τ...) draw!(img::AbstractArray{T,2}, line::LineNormal, method::Function = bresenham) where {T<:Colorant} = - draw!(img, line, oneunit(T), method) + draw!(img, line, oneunit(T), 1, method) -function draw!(img::AbstractArray{T, 2}, line::LineNormal, color::T, method::Function = bresenham) where T<:Colorant +function draw!(img::AbstractArray{T, 2}, line::LineNormal, color::T, thickness::Int=1, method::Function = bresenham) where T<:Colorant indsy, indsx = axes(img) cosθ = cos(line.θ) sinθ = sin(line.θ) @@ -50,7 +52,7 @@ function draw!(img::AbstractArray{T, 2}, line::LineNormal, color::T, method::Fun intersections_y = [((line.ρ - y*sinθ)/cosθ, y) for y in (first(indsy), last(indsy))] valid_intersections = get_valid_intersections(vcat(intersections_x, intersections_y), indsx, indsy) if length(valid_intersections) > 0 - method(img, round(Int,valid_intersections[1][2]), round(Int,valid_intersections[1][1]), round(Int,valid_intersections[2][2]), round(Int,valid_intersections[2][1]), color) + method(img, round(Int,valid_intersections[1][2]), round(Int,valid_intersections[1][1]), round(Int,valid_intersections[2][2]), round(Int,valid_intersections[2][1]), color, thickness) else img end @@ -62,14 +64,16 @@ LineSegment(x0::Int, y0::Int, x1::Int, y1::Int) = LineSegment(Point(x0, y0), Poi LineSegment(p1::CartesianIndex, p2::CartesianIndex) = LineSegment(Point(p1), Point(p2)) draw!(img::AbstractArray{T,2}, line::LineSegment, method::Function = bresenham) where {T<:Colorant} = - draw!(img, line, oneunit(T), method) + draw!(img, line, oneunit(T), 1, method) draw!(img::AbstractArray{T,2}, line::LineSegment, color::T, method::Function = bresenham) where {T<:Colorant} = - method(img, line.p1.y, line.p1.x, line.p2.y, line.p2.x, color) + draw!(img, line, color, 1, method) -# Methods to draw lines +draw!(img::AbstractArray{T,2}, line::LineSegment, color::T, thickness::Int, method::Function = bresenham) where {T<:Colorant} = + method(img, line.p1.y, line.p1.x, line.p2.y, line.p2.x, color, thickness) -function bresenham(img::AbstractArray{T, 2}, y0::Int, x0::Int, y1::Int, x1::Int, color::T) where T<:Colorant +# Methods to draw lines +function bresenham(img::AbstractArray{T, 2}, y0::Int, x0::Int, y1::Int, x1::Int, color::T, thickness::Int=1) where T<:Colorant dx = abs(x1 - x0) dy = abs(y1 - y0) @@ -79,7 +83,7 @@ function bresenham(img::AbstractArray{T, 2}, y0::Int, x0::Int, y1::Int, x1::Int, err = (dx > dy ? dx : -dy) / 2 while true - drawifinbounds!(img, y0, x0, color) + drawwiththickness!(img, y0, x0, color, thickness) (x0 != x1 || y0 != y1) || break e2 = err if e2 > -dx @@ -95,6 +99,7 @@ function bresenham(img::AbstractArray{T, 2}, y0::Int, x0::Int, y1::Int, x1::Int, img end + fpart(pixel::T) where {T} = pixel - T(trunc(pixel)) rfpart(pixel::T) where {T} = oneunit(T) - fpart(pixel) @@ -102,7 +107,7 @@ function swap(x, y) y, x end -function xiaolin_wu(img::AbstractArray{T, 2}, y0::Int, x0::Int, y1::Int, x1::Int, color::T) where T<:Gray +function xiaolin_wu(img::AbstractArray{T, 2}, y0::Int, x0::Int, y1::Int, x1::Int, color::T, thickness::Int=1) where T<:Gray dx = x1 - x0 dy = y1 - y0 diff --git a/src/paths.jl b/src/paths.jl index 02bcd8b..ac67704 100644 --- a/src/paths.jl +++ b/src/paths.jl @@ -3,10 +3,13 @@ Path(v::AbstractVector{Tuple{Int, Int}}) = Path([Point(p...) for p in v]) Path(v::AbstractVector{CartesianIndex{2}}) = Path([Point(p) for p in v]) -function draw!(img::AbstractArray{T, 2}, path::Path, color::T) where T<:Colorant +draw!(img::AbstractArray{T, 2}, path::Path, thickness::Int) where T<:Colorant = + draw!(img, path, oneunit(T), thickness) + +function draw!(img::AbstractArray{T, 2}, path::Path, color::T, thickness::Int=1) where T<:Colorant vertices = [CartesianIndex(p.y, p.x) for p in path.vertices] for i in 1:length(vertices)-1 - draw!(img, LineSegment(vertices[i], vertices[i+1]), color) + draw!(img, LineSegment(vertices[i], vertices[i+1]), color, thickness) end img end @@ -16,9 +19,12 @@ end Polygon(v::AbstractVector{Tuple{Int, Int}}) = Polygon([Point(p...) for p in v]) Polygon(v::AbstractVector{CartesianIndex{2}}) = Polygon([Point(p) for p in v]) -function draw!(img::AbstractArray{T, 2}, polygon::Polygon, color::T) where T<:Colorant - draw!(img, Path(polygon.vertices), color) - draw!(img, LineSegment(first(polygon.vertices), last(polygon.vertices)), color) +draw!(img::AbstractArray{T, 2}, polygon::Polygon, thickness::Int) where T<:Colorant = + draw!(img, path, oneunit(T), thickness) + +function draw!(img::AbstractArray{T, 2}, polygon::Polygon, color::T, thickness::Int=1) where T<:Colorant + draw!(img, Path(polygon.vertices), color, thickness) + draw!(img, LineSegment(first(polygon.vertices), last(polygon.vertices)), color, thickness) end #RegularPolygon methods @@ -26,9 +32,12 @@ end RegularPolygon(point::CartesianIndex{2}, side_count::Int, side_length::T, θ::U) where {T<:Real, U<:Real} = RegularPolygon(Point(point), side_count, side_length, θ) -function draw!(img::AbstractArray{T, 2}, rp::RegularPolygon, color::T) where T<:Colorant +draw!(img::AbstractArray{T, 2}, rp::RegularPolygon, thickness::Int) where T<:Colorant = + draw!(img, rp, oneunit(T), thickness) + +function draw!(img::AbstractArray{T, 2}, rp::RegularPolygon, color::T, thickness::Int=1) where T<:Colorant n = rp.side_count ρ = rp.side_length/(2*sin(π/n)) polygon = Polygon([ Point(round(Int, rp.center.x + ρ*cos(rp.θ + 2π*k/n)), round(Int, rp.center.y + ρ*sin(rp.θ + 2π*k/n))) for k in 1:n ]) - draw!(img, polygon, color) + draw!(img, polygon, color, thickness) end diff --git a/test/background.jl b/test/background.jl new file mode 100644 index 0000000..9289259 --- /dev/null +++ b/test/background.jl @@ -0,0 +1,54 @@ + +@testset "Background" begin + @testset "SolidBackground" begin + + res = generatecanvas(Gray{N0f8}, 10,10) + expected = zeros(Gray{N0f8}, (10,10)) + + @test all(expected .== res) == true + + res = generatecanvas(Gray{N0f8}, 10,10, SolidBackground(Gray{N0f8}(1.0))) + expected = ones(Gray{N0f8}, (10,10)) + + @test all(expected .== res) == true + + + end + + @testset "StripedBackground" begin + + res = generatecanvas(Gray{N0f8}, (10,10), StripedBackground([Gray{N0f8}(1.0),Gray{N0f8}(.5), Gray{N0f8}(.2)], [2, 5, 8], pi/2)) + expected = Gray{N0f8}[ 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 + 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 + 0.502 0.502 0.502 0.502 0.502 0.502 0.502 0.502 0.502 0.502 + 0.502 0.502 0.502 0.502 0.502 0.502 0.502 0.502 0.502 0.502 + 0.502 0.502 0.502 0.502 0.502 0.502 0.502 0.502 0.502 0.502 + 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 + 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 + 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 + 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 ] + + @test all(expected .== res) == true + + res = generatecanvas(RGB, (5,5), StripedBackground([RGB(.3,.6,.7),RGB(.5,.2,.9)], [2, 4], 0)) + expected = RGB[ RGB(.3,.6,.7) RGB(.3,.6,.7) RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(0,0,0) + RGB(.3,.6,.7) RGB(.3,.6,.7) RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(0,0,0) + RGB(.3,.6,.7) RGB(.3,.6,.7) RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(0,0,0) + RGB(.3,.6,.7) RGB(.3,.6,.7) RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(0,0,0) + RGB(.3,.6,.7) RGB(.3,.6,.7) RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(0,0,0) ] + + @test all(expected .== res) == true + + res = generatecanvas(RGB, (5,5), StripedBackground([RGB(.3,.6,.7),RGB(.5,.2,.9)], [2, 4], pi/4)) + expected = RGB[ RGB(.3,.6,.7) RGB(.3,.6,.7) RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(.5,.2,.9) + RGB(.3,.6,.7) RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(0,0,0) + RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(0,0,0) RGB(0,0,0) + RGB(.5,.2,.9) RGB(.5,.2,.9) RGB(0,0,0) RGB(0,0,0) RGB(0,0,0) + RGB(.5,.2,.9) RGB(0,0,0) RGB(0,0,0) RGB(0,0,0) RGB(0,0,0) ] + + @test all(expected .== res) == true + + end + +end diff --git a/test/runtests.jl b/test/runtests.jl index 5aaca03..7ac4531 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,7 +9,8 @@ tests = [ "ellipse2d.jl", "circle2d.jl", "paths.jl", - "cross.jl" + "cross.jl", + "background.jl" ] for t in tests