Skip to content

Commit

Permalink
Merge pull request #5 from salam99823/main
Browse files Browse the repository at this point in the history
Rework of `edges`
  • Loading branch information
shnewto authored Nov 11, 2024
2 parents 679be82 + bd9591f commit 0617e9f
Show file tree
Hide file tree
Showing 11 changed files with 807 additions and 398 deletions.
27 changes: 18 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,31 @@ repository = "https://github.com/shnewto/edges"

license = "MIT OR Apache-2.0"

[lints.clippy]
cast_precision_loss = { level = "allow", priority = 1 }
pedantic = { level = "warn", priority = 0 }

[features]
default=[]
bevy=["dep:bevy"]
default = ["bevy"]
glam-latest = ["dep:glam"]
bevy = ["dep:bevy_math", "dep:bevy_render"]

[dependencies]
glam = "0.27.0"
hashbrown = "0.14"
image = "0.25"
mashmap = "0.1"
ordered-float = "4.2"
thiserror = "1.0"
rayon = "1.10.0"

[dependencies.glam]
version = "0.29"
optional = true

[dependencies.bevy_math]
version = "0.14"
default-features = false
optional = true

[dependencies.bevy]
[dependencies.bevy_render]
version = "0.14"
default-features = false
features = ["bevy_render"]
optional = true

[dev-dependencies]
Expand Down
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[![Crates.io](<https://img.shields.io/crates/v/edges.svg>)](<https://crates.io/crates/edges>)
[![Crates.io](<https://img.shields.io/crates/d/edges.svg>)](<https://crates.io/crates/edges>)
[![MIT/Apache 2.0](<https://img.shields.io/badge/license-MIT%2FApache-blue.svg>)](<https://github.com/shnewto/edges#license>)

# edges

[![Crates.io](https://img.shields.io/crates/v/edges.svg)](https://crates.io/crates/edges)
[![Crates.io](https://img.shields.io/crates/d/edges.svg)](https://crates.io/crates/edges)
[![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/shnewto/edges#license)

get the edges of objects in images with transparency.

## supported image types
Expand All @@ -24,13 +24,20 @@ println!("{:#?}", edges.single_image_edge_translated());

## how it works

i was inspired by [a coding train (or, coding in the cabana rather) on an implementation of "marching squares"](<https://youtu.be/0ZONMNUKTfU>).
so this crate takes a "march through all the values" approach to find edges, i.e. pixels with at least 1 empty neighboring pixel, but
instead of drawing a contour in place, it just keeps track of all the actual pixel coordinates. to determine "empty" I bitwise
or all the bytes for each pixel and, in images with transparency, "empty" is a zero value for the pixel.

after that, we need to put the coordinates in some kind of "drawing order" so whatever we pass all the points to, knows how we want the object constructed. for this, the
crate collects all pixels, in order, that are a distance of 1 from eachother. if there are pixels that have a distance greater than 1
i was inspired by [a coding train (or, coding in the cabana rather)
on an implementation of "marching squares"](https://youtu.be/0ZONMNUKTfU).
so this crate takes a "march through all the values" approach to find edges, i.e.
pixels with at least 1 empty neighboring pixel, but
instead of drawing a contour in place,
it just keeps track of all the actual pixel coordinates. to determine "empty" I bitwise
or all the bytes for each pixel and,
in images with transparency, "empty" is a zero value for the pixel.

after that, we need to put the coordinates in some kind of
"drawing order" so whatever we pass all the points to,
knows how we want the object constructed. for this, the
crate collects all pixels, in order, that are a distance of 1 from eachother.
if there are pixels that have a distance greater than 1
from any pixel in an existing group, that pixel begins a new group.

## license
Expand Down
Binary file added assets/diagonals.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 36 additions & 17 deletions examples/bevy-image.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use bevy::{prelude::Image, render::texture::ImageType};
use bevy_render::{
prelude::Image,
render_asset::RenderAssetUsages,
texture::{CompressedImageFormats, ImageSampler, ImageType},
};
use edges::Edges;
use raqote::*;
use raqote::{DrawOptions, DrawTarget, PathBuilder, SolidSource, Source, StrokeStyle};
// in an actual bevy app, you wouldn't need all this building an Image from scratch logic,
// it'd be something closer to this:
// `let image = image_assets.get(handle).unwrap();`
Expand All @@ -11,39 +15,54 @@ fn main() {
let boulders = Image::from_buffer(
include_bytes!("../assets/boulders.png"),
ImageType::Extension("png"),
Default::default(),
CompressedImageFormats::default(),
true,
Default::default(),
Default::default(),
ImageSampler::default(),
RenderAssetUsages::default(),
)
.unwrap();

let more_lines = Image::from_buffer(
include_bytes!("../assets/more-lines.png"),
ImageType::Extension("png"),
Default::default(),
CompressedImageFormats::default(),
true,
Default::default(),
Default::default(),
ImageSampler::default(),
RenderAssetUsages::default(),
)
.unwrap();

draw_png(boulders, "boulders.png");
draw_png(more_lines, "more-lines.png");
let diagonals = Image::from_buffer(
include_bytes!("../assets/diagonals.png"),
ImageType::Extension("png"),
CompressedImageFormats::default(),
true,
ImageSampler::default(),
RenderAssetUsages::default(),
)
.unwrap();

draw_png(&boulders, "boulders");
draw_png(&more_lines, "more-lines");
draw_png(&diagonals, "diagonals");
}

fn draw_png(image: Image, img_path: &str) {
fn draw_png(image: &Image, img_path: &str) {
// get the image's edges
let edges = Edges::from(image.clone());
let edges = Edges::from(image);

let scale = 8;
let (width, height) = (image.width() as i32 * scale, image.height() as i32 * scale);
let (width, height) = (
i32::try_from(image.width()).expect("Image to wide.") * scale,
i32::try_from(image.height()).expect("Image to tall.") * scale,
);

// draw the edges to a png
let mut dt = DrawTarget::new(width, height);

let objects_iter = edges.multi_image_edges_raw().into_iter();
let objects = edges.multi_image_edges_raw();

for object in objects_iter {
for object in objects {
let mut pb = PathBuilder::new();
let mut edges_iter = object.into_iter();

Expand Down Expand Up @@ -71,6 +90,6 @@ fn draw_png(image: Image, img_path: &str) {
);
}

dt.write_png(format!("edges-{}", img_path)).unwrap();
_ = open::that(format!("edges-{}", img_path));
dt.write_png(format!("edges-{img_path}.png")).unwrap();
_ = open::that(format!("edges-{img_path}.png"));
}
17 changes: 11 additions & 6 deletions examples/dynamic-image.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use edges::Edges;
use raqote::*;
use raqote::{DrawOptions, DrawTarget, PathBuilder, SolidSource, Source, StrokeStyle};
use std::path::Path;

fn main() {
Expand All @@ -9,10 +9,15 @@ fn main() {
}

fn draw_png(img_path: &str) {
let image = &image::open(Path::new(&format!("assets/{}", img_path))).unwrap();
let edges = Edges::from(image);
let image = image::open(Path::new(&format!("assets/{img_path}"))).unwrap();
// get the image's edges
let edges = Edges::from(&image);

let scale = 8;
let (width, height) = (image.width() as i32 * scale, image.height() as i32 * scale);
let (width, height) = (
i32::try_from(image.width()).expect("Image to wide.") * scale,
i32::try_from(image.height()).expect("Image to tall.") * scale,
);

// draw the edges to a png
let mut dt = DrawTarget::new(width, height);
Expand Down Expand Up @@ -41,6 +46,6 @@ fn draw_png(img_path: &str) {
&DrawOptions::new(),
);

dt.write_png(format!("edges-{}", img_path)).unwrap();
_ = open::that(format!("edges-{}", img_path));
dt.write_png(format!("edges-{img_path}")).unwrap();
_ = open::that(format!("edges-{img_path}"));
}
161 changes: 161 additions & 0 deletions src/bin_image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use crate::{utils::is_corner, UVec2, Vec2};
use rayon::prelude::*;
pub mod neighbors {
pub const NORTH: u8 = 0b1000_0000;
pub const SOUTH: u8 = 0b0100_0000;
pub const EAST: u8 = 0b0010_0000;
pub const WEST: u8 = 0b0001_0000;
pub const NORTHEAST: u8 = 0b0000_1000;
pub const NORTHWEST: u8 = 0b0000_0100;
pub const SOUTHEAST: u8 = 0b0000_0010;
pub const SOUTHWEST: u8 = 0b0000_0001;
}

pub struct BinImage {
data: Vec<u8>,
height: u32,
width: u32,
}

impl BinImage {
/// Creates a new `BinImage` from the given height, width, and raw pixel data.
///
/// # Arguments
///
/// * `height` - The height of the image in pixels.
/// * `width` - The width of the image in pixels.
/// * `data` - A slice of bytes representing the raw pixel data. The length of this slice
/// must be at least `height * width`.
///
/// # Panics
///
/// This function will panic if the length of `data` is less than `height * width`.
pub fn new(height: u32, width: u32, data: &[u8]) -> Self {
assert!(
data.len() >= (height * width) as usize,
"data must not be smaller than image dimensions"
);
let compress_step = data.len() / (height * width) as usize;
Self {
data: data
.par_chunks(8 * compress_step)
.map(|chunk| {
chunk
.par_chunks(compress_step)
.map(|chunk| chunk.iter().any(|i| *i != 0))
.enumerate()
.map(|(index, bit)| u8::from(bit) << index)
.sum()
})
.collect(),
height,
width,
}
}

/// Gets the pixel value at the given coordinate.
///
/// # Arguments
///
/// * `p` - A `UVec2` representing the coordinates of the pixel.
///
/// # Returns
///
/// Returns `true` if the pixel is "on" (1), and `false` if it is "off" (0) or out of bounds.
pub fn get(&self, p: UVec2) -> bool {
if p.x >= self.width {
return false;
}
let index = p.y * self.width + p.x;
if let Some(mut byte) = self
.data
.get((index / 8) as usize) // index of byte
.copied()
{
byte >>= index % 8; // index of bit
byte & 1 > 0
} else {
false
}
}

/// Gets the values of the neighboring pixels (8-connectivity) around the given coordinate.
///
/// # Arguments
///
/// * `p` - A `UVec2` representing the coordinates of the center pixel.
///
/// # Returns
///
/// An byte representing the state of the neighboring pixels.
pub fn get_neighbors(&self, p: UVec2) -> u8 {
let (x, y) = (p.x, p.y);
let mut neighbors = 0;
if y < u32::MAX && self.get(UVec2::new(x, y + 1)) {
neighbors |= neighbors::NORTH;
}
if y > u32::MIN && self.get(UVec2::new(x, y - 1)) {
neighbors |= neighbors::SOUTH;
}
if x < u32::MAX && self.get(UVec2::new(x + 1, y)) {
neighbors |= neighbors::EAST;
}
if x > u32::MIN && self.get(UVec2::new(x - 1, y)) {
neighbors |= neighbors::WEST;
}
if x < u32::MAX && y < u32::MAX && self.get(UVec2::new(x + 1, y + 1)) {
neighbors |= neighbors::NORTHEAST;
}
if x > u32::MIN && y < u32::MAX && self.get(UVec2::new(x - 1, y + 1)) {
neighbors |= neighbors::NORTHWEST;
}
if x < u32::MAX && y > u32::MIN && self.get(UVec2::new(x + 1, y - 1)) {
neighbors |= neighbors::SOUTHEAST;
}
if x > u32::MIN && y > u32::MIN && self.get(UVec2::new(x - 1, y - 1)) {
neighbors |= neighbors::SOUTHWEST;
}
neighbors
}

pub fn is_corner(&self, p: UVec2) -> bool {
is_corner(self.get_neighbors(p))
}

/// Translates a point in positive (x, y) coordinates to a coordinate system centered at (0, 0).
///
/// # Arguments
///
/// * `p` - A `Vec2` representing the point to translate.
///
/// # Returns
///
/// A new `Vec2` representing the translated coordinates
fn translate_point(&self, p: Vec2) -> Vec2 {
Vec2::new(
p.x - ((self.width / 2) as f32 - 1.0),
((self.height / 2) as f32 - 1.0) - p.y,
)
}

/// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0).
///
/// # Arguments
///
/// * `v` - An `Vec` of `Vec2` points to translate.
///
/// # Returns
///
/// A vector of `Vec2` representing the translated coordinates.
pub fn translate(&self, v: Vec<Vec2>) -> Vec<Vec2> {
v.into_par_iter().map(|p| self.translate_point(p)).collect()
}

pub const fn height(&self) -> u32 {
self.height
}

pub const fn width(&self) -> u32 {
self.width
}
}
Loading

0 comments on commit 0617e9f

Please sign in to comment.