Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encode Operation #6

Merged
merged 8 commits into from
Oct 2, 2022
Merged
7 changes: 5 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ ColorVectorSpace = "c3611d14-8923-5661-9e6a-0046d554d3a4"
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
Giflib_jll = "59f7168a-df46-5410-90c8-f2779963d0ec"
RegionTrees = "dee08c22-ab7f-5625-9660-a9af2021b33f"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
libgifextra_jll = "d7c3d077-fdb7-5318-9663-3e6817e0e281"

[compat]
julia = "1.7"
CEnum = "0.4.2"
ColorTypes = "0.11.3"
ColorVectorSpace = "0.9.9"
FileIO = "1.14.0"
FixedPointNumbers = "0.8.4"
Giflib_jll = "5.2.1"
libgifextra_jll = "0.0.1"
julia = "1.7"
libgifextra_jll = "0.0.1"
21 changes: 21 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ julia> img = gif_decode(path)
60×30×33 Array{RGB{N0f8},3} with eltype RGB{N0f8}
```

---
For encoding, GIFImages.jl provides `gif_encode` which encode the GIF colorant matrix to file.

#### Arguments
- `filepath` : Name of the file to which image is written.
- `img` : 3D GIF colorant matrix which has structure of height* width * numofimages and all the images are present as slices of the 3D matrix
- `colormapnum` : Specifies the number of colors to be used for the global colormap

### Examples
```jldoctest
julia> using GIFImages, Downloads

julia> path = "test/data/fire.gif"
"test/data/fire.gif"

julia> img = gif_decode(path)
60×30×33 Array{RGB{N0f8},3} with eltype RGB{N0f8}

julia> gif_encode("fire.gif", img)
```

```@autodocs
Modules = [GIFImages]
```
13 changes: 11 additions & 2 deletions src/GIFImages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ include("../lib/LibGifExtra.jl")

using .LibGif
using .LibGifExtra
using ColorTypes
using ColorVectorSpace
using FixedPointNumbers
using StatsBase
using RegionTrees, StaticArrays

include("decode.jl")

export gif_decode
include("decode.jl")
include("encode.jl")
include("quantizers.jl")

export gif_decode, gif_encode
export split_buckets, mediancut!, mediancut
export octreequantisation, octreequantisation!

end # module
4 changes: 0 additions & 4 deletions src/decode.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
using ColorTypes
using ColorVectorSpace
using FixedPointNumbers
using FileIO

"""
gif_decode(filepath::AbstractString; use_localpalette=false)
Expand Down
74 changes: 74 additions & 0 deletions src/encode.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@


"""
gif_encode(filepath::AbstractString, img::AbstractArray; num::Int = 64)

Encode the GIF colorant matrix to file.

#### Arguments
- `filepath` : Name of the file to which image is written.
- `img` : 3D GIF colorant matrix which has structure of height*width*numofimags and all the images are present as slices of the 3D matrix
- `colormapnum` : Specifies the number of colors to be used for the global colormap

### Examples
```jldoctest
julia> using GIFImages, Downloads

julia> path = "test/data/fire.gif"
"test/data/fire.gif"

julia> img = gif_decode(path)
60×30×33 Array{RGB{N0f8},3} with eltype RGB{N0f8}

julia> gif_encode("fire.gif", img)
```
"""
function gif_encode(filepath::AbstractString, img::AbstractArray; colormapnum::Int = 256)
error = Cint(0)
gif_file = LibGif.EGifOpenFileName(filepath, 0, Ref(error))
ashwani-rathee marked this conversation as resolved.
Show resolved Hide resolved
colors = []

gif_file == C_NULL && error("EGifOpenFileName() failed to open the gif file: null pointer")

# checks if colormapnum is in valid range
if (colormapnum < 1 || colormapnum > 256)
error("colormapnum is out of range and needs to be in range 1-256(both inclusive)")
end

# generation of a colormap
palette = octreequantisation!(img; numcolors=colormapnum, return_palette=true)
append!(colors, palette)

mapping = Dict()
for (i, j) in enumerate(colors)
mapping[j] = UInt8(i)
end

# defining the global colormap
colors = map(x -> LibGif.GifColorType(x.r, x.g, x.b), colors * 255)
ashwani-rathee marked this conversation as resolved.
Show resolved Hide resolved
colormap = LibGif.GifMakeMapObject(colormapnum, colors)

# features of the file
gif_file.SWidth = size(img)[2]
gif_file.SHeight = size(img)[1]
gif_file.SColorResolution = 8
gif_file.SBackGroundColor = 0
gif_file.SColorMap = colormap

# encoding
for i = 1:size(img)[3]
# flatten the image
img1 = vec(img[:, :, i]')
ashwani-rathee marked this conversation as resolved.
Show resolved Hide resolved
pix = map(x -> mapping[x], img1)

# saving a new image in gif_file
ashwani-rathee marked this conversation as resolved.
Show resolved Hide resolved
desc = LibGif.GifImageDesc(0, 0, size(img)[2], size(img)[1], 0, C_NULL)
c = LibGif.SavedImage(desc, pointer(pix), 0, C_NULL)
LibGif.GifMakeSavedImage(gif_file, Ref(c))
end

# writing and closing the file
if (LibGif.EGifSpew(gif_file) == LibGif.GIF_ERROR)
error("Failed to write to file!")
end
end
153 changes: 153 additions & 0 deletions src/quantizers.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# In this file, we have the color quantisation algorithms

function split_buckets(img, data, depth)
if length(data) == 0 return end

# Buckets are same size,
# means that each colors is assigned to
# equal number of pixels here.
if depth == 0
# this behavior is interesting of N0f8
# and of floats as it's messing up results
avg = RGB{N0f8}.(mean(map(x -> x[2], data)))
map(x -> img[x[1]] = avg, data)
return
end

# find range of colors and pick color which
# most difference in max val and min val
rmin, rmax = 1.0N0f8, 0.0N0f8
gmin, gmax = 1.0N0f8, 0.0N0f8
bmin, bmax = 1.0N0f8, 0.0N0f8
for (idx, color) in data
if (color.r > rmax) rmax = color.r end
if (color.g > gmax) gmax = color.g end
if (color.b > bmax) bmax = color.b end
if (color.r < rmin) rmin = color.r end
if (color.g < gmin) gmin = color.g end
if (color.b < bmin) bmin = color.b end
end

ind = findmax([rmax - rmin, gmax - gmin, bmax - bmin])[2]

# sort on basis of max range color
if ind == 1 sort!(data; by = c -> c[2].r)
ashwani-rathee marked this conversation as resolved.
Show resolved Hide resolved
elseif ind == 2 sort!(data; by = c -> c[2].g)
elseif ind == 3 sort!(data; by = c -> c[2].b) end

# start diving on basis of median index
medind = trunc(Int, (length(data) + 1) / 2)

# two separate buckets
split_buckets(img, data[1:medind], depth - 1)
ashwani-rathee marked this conversation as resolved.
Show resolved Hide resolved
split_buckets(img, data[medind+1:end], depth - 1)
end


function mediancut!(img, max = 6)
data = map(x -> x, enumerate(vec(img)))
split_buckets(img, data, max)
end

function mediancut(img, max = 6)
img1 = deepcopy(img)
mediancut!(img1, max)
return img1
end

function putin(root, in)
color_in = in[2]
r, g, b = map(p->bitstring(UInt8(p*255)), [color_in.r, color_in.g, color_in.b])

# finding the entry to the tree
ind = 0
for i = 1:8
if (root.children[i].data[1] == r[1] * g[1] * b[1])
root.children[i].data[2] += 1
ind = i
break
end
end
curr = root.children[ind]

for i = 2:8
cases = map(p->[bitstring(UInt8(p))[6:end], 0, Vector{Int}([]), RGB{N0f8}.(0.0,0.0,0.0), i], 1:8)
if (isleaf(curr) == true && i < 8) split!(curr, cases) end
if (i == 8)
if (isleaf(curr) == true) split!(curr, cases) end
for j = 1:8
if (curr.children[j].data[1] == r[i] * g[i] * b[i])
curr = curr.children[j]
curr.data[2] += 1
push!(curr.data[3], in[1])
curr.data[4] = in[2]
return
end
end
end

# handle 1:7 cases for rgb to handle first seven bits
for j = 1:8
if (curr.children[j].data[1] == r[i] * g[i] * b[i])
curr.children[j].data[2] += 1
curr = curr.children[j]
break
end
end
end
end


function octreequantisation!(img; numcolors = 256, return_palette = false )
# step 1: creating the octree
root = Cell(SVector(0.0, 0.0, 0.0), SVector(1.0, 1.0, 1.0), ["root", 0, [], RGB{N0f8}.(0.0, 0.0, 0.0), 0])
cases = map(p->[bitstring(UInt8(p))[6:end], 0, Vector{Int}([]), RGB{N0f8}.(0.0,0.0,0.0), 1], 1:8)
split!(root, cases)
for i in enumerate(img)
root.data[2]+=1
putin(root, i)
end

# step 2: reducing tree to a certain number of colors
# there is scope for improvements in allleaves as it's found again n again
leafs = [p for p in allleaves(root)]
filter!(p -> !iszero(p.data[2]), leafs)
tobe_reduced = leafs[1]

while (length(leafs) > numcolors)
parents = unique([parent(p) for p in leafs])
parents = sort(parents; by = c -> c.data[2])
tobe_reduced = parents[1]

for i = 1:8
append!(tobe_reduced.data[3], tobe_reduced.children[i].data[3])
tobe_reduced.data[4] += tobe_reduced.children[i].data[4] * tobe_reduced.children[i].data[2]
end
tobe_reduced.data[4] /= tobe_reduced.data[2]
tobe_reduced.children = nothing

# we don't want to do this again n again
leafs = [p for p in allleaves(root)]
filter!(p -> !iszero(p.data[2]), leafs)
end

# step 3: palette formation and quantisation now
da = [p.data for p in leafs]
for i in da
for j in i[3]
img[j] = i[4]
end
end

if return_palette == true
colors = [p[4] for p in da]
return colors
end
end

function octreequantisation(img; kwargs...)
img1 = deepcopy(img)
# possible error likely here to return palette kwarg
octreequantisation!(img1; kwargs...)
return img1
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems perfectly a case that can be implemented in a separate standalone package so that other packages (e.g., DitherPunk) can benefit from it.

cc: @adrhill

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this would be very useful and I'd be very interested in contributing to (or writing) such a package. :)

Copy link
Member

@johnnychen94 johnnychen94 Sep 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small discussion on slack: because this package along with the other two https://github.com/ashwani-rathee/ExifViewer.jl and https://github.com/ashwani-rathee/JPEG2000.jl are part of GSoC 22', we'll quickly get this PR merged and then task switch to JPEG2000. So these quantization methods will be temporarily kept private to GIFImages.

Just opened an issue so that we don't forget this #7

18 changes: 18 additions & 0 deletions test/encode.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@testset "Encoding" begin

@testset "Basic Encoding" begin
img1 = gif_decode(get_example("fire.gif"))
gif_encode("test1.gif", img1)
@test size(gif_decode("test1.gif")) == (60,30,33)

img1 = gif_decode(get_example("gifgrid.gif"))
gif_encode("test1.gif", img1)
@test size(gif_decode("test1.gif")) == (100,100,1)
end

@testset "Encode, Decode compatibility" begin
# ensure encodes are same as what was decoded

# major issue here
end
end
17 changes: 17 additions & 0 deletions test/quantizers.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@testset "Quantizers" begin

@testset "Median Cut Color Quantizer" begin
img = gif_decode(get_example("fire.gif"))
@test length(unique(mediancut(img, 5))) == 16

# test on different color ColorTypes

# test on unique colors less than asked amount
end

@testset "Octree Color Quantizer" begin
img = gif_decode(get_example("fire.gif"))
octreequantisation!(img; numcolors=120)
@test length(unique(img)) == 120
end
end
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ _wrap(name) = "https://github.com/ashwani-rathee/gif-sampleimages/blob/main/$(na
get_example(x) = Downloads.download(_wrap(x))

include("decode.jl")
include("encode.jl")
include("quantizers.jl")