Skip to content

Add Cropping layers support #1309

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

Merged
merged 4 commits into from
Jun 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion hls4ml/backends/vivado/passes/reshaping_templates.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
from hls4ml.model.layers import Resize, Transpose, ZeroPadding1D, ZeroPadding2D
from hls4ml.model.layers import Cropping1D, Cropping2D, Resize, Transpose, ZeroPadding1D, ZeroPadding2D
from hls4ml.utils.transpose_utils import transpose_config_gen

# ZeroPadding templates
Expand Down Expand Up @@ -145,3 +145,61 @@ def format(self, node):
params = self._default_function_params(node)
params['config_name'] = f'config{node.index}'
return self.template.format(**params)


# Cropping templates


cropping1d_config_template = """struct config{index} : nnet::cropping1d_config {{
static const unsigned in_width = {in_width};
static const unsigned n_chan = {n_chan};
static const unsigned out_width = {out_width};
static const unsigned crop_left = {crop_left};
static const unsigned crop_right = {crop_right};
}};\n"""

cropping2d_config_template = """struct config{index} : nnet::cropping2d_config {{
static const unsigned in_height = {in_height};
static const unsigned in_width = {in_width};
static const unsigned n_chan = {n_chan};
static const unsigned out_height = {out_height};
static const unsigned out_width = {out_width};
static const unsigned crop_top = {crop_top};
static const unsigned crop_bottom = {crop_bottom};
static const unsigned crop_left = {crop_left};
static const unsigned crop_right = {crop_right};
}};\n"""

cropping1d_function_template = 'nnet::cropping1d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'
cropping2d_function_template = 'nnet::cropping2d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'

cropping_include_list = ['nnet_utils/nnet_cropping.h', 'nnet_utils/nnet_cropping_stream.h']


class CroppingConfigTemplate(LayerConfigTemplate):
def __init__(self):
super().__init__((Cropping1D, Cropping2D))
self.templates = {
'Cropping1D': cropping1d_config_template,
'Cropping2D': cropping2d_config_template,
}

def format(self, node):
params = self._default_config_params(node)
return self.templates[node.class_name].format(**params)


class CroppingFunctionTemplate(FunctionCallTemplate):
def __init__(self):
super().__init__((Cropping1D, Cropping2D), include_header=cropping_include_list)
self.templates = {
'Cropping1D': cropping1d_function_template,
'Cropping2D': cropping2d_function_template,
}

def format(self, node):
params = self._default_function_params(node)
# Cropping1D doesn't have a data_format attribute
params['data_format'] = 'cf' if node.get_attr('data_format') == 'channels_first' else 'cl'

return self.templates[node.class_name].format(**params)
96 changes: 96 additions & 0 deletions hls4ml/converters/keras/reshaping.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,99 @@ def parse_zeropadding2d_layer(keras_layer, input_names, input_shapes, data_reade
layer['in_width'] = input_shapes[0][2]

return layer, output_shape


@keras_handler('Cropping1D')
def parse_cropping1d_layer(keras_layer, input_names, input_shapes, data_reader):
assert keras_layer['class_name'] == 'Cropping1D'

layer = parse_default_keras_layer(keras_layer, input_names)

cropping = keras_layer['config']['cropping']
if isinstance(cropping, int):
layer['crop_left'] = cropping
layer['crop_right'] = cropping
elif isinstance(cropping, collections.abc.Sequence):
layer['crop_left'] = cropping[0]
layer['crop_right'] = cropping[1]

# No data_format attribute for Cropping1D (always cl), but keeping it consistent with Cropping2D
if layer['data_format'] == 'channels_first':
output_shape = [
input_shapes[0][0], # Batch
input_shapes[0][1], # Channels
input_shapes[0][2] - layer['crop_left'] - layer['crop_right'], # Width
]
layer['out_width'] = output_shape[2]
layer['n_chan'] = output_shape[1]

layer['in_width'] = input_shapes[0][2]
else:
output_shape = [
input_shapes[0][0], # Batch
input_shapes[0][1] - layer['crop_left'] - layer['crop_right'], # Width
input_shapes[0][2], # Channels
]
layer['out_width'] = output_shape[1]
layer['n_chan'] = output_shape[2]

layer['in_width'] = input_shapes[0][1]

return layer, output_shape


@keras_handler('Cropping2D')
def parse_cropping2d_layer(keras_layer, input_names, input_shapes, data_reader):
assert keras_layer['class_name'] == 'Cropping2D'

layer = parse_default_keras_layer(keras_layer, input_names)

cropping = keras_layer['config']['cropping']
if isinstance(cropping, int):
layer['crop_top'] = cropping
layer['crop_bottom'] = cropping
layer['crop_left'] = cropping
layer['crop_right'] = cropping
elif isinstance(cropping, collections.abc.Sequence):
height_crop, width_crop = cropping
if isinstance(height_crop, collections.abc.Sequence):
layer['crop_top'] = height_crop[0]
layer['crop_bottom'] = height_crop[1]
else:
layer['crop_top'] = height_crop
layer['crop_bottom'] = height_crop
if isinstance(width_crop, collections.abc.Sequence):
layer['crop_left'] = width_crop[0]
layer['crop_right'] = width_crop[1]
else:
layer['crop_left'] = width_crop
layer['crop_right'] = width_crop

if layer['data_format'] == 'channels_first':
output_shape = [
input_shapes[0][0], # Batch
input_shapes[0][1], # Channels
input_shapes[0][2] - layer['crop_top'] - layer['crop_bottom'], # Height
input_shapes[0][3] - layer['crop_left'] - layer['crop_right'], # Width
]
layer['out_height'] = output_shape[2]
layer['out_width'] = output_shape[3]
layer['n_chan'] = output_shape[1]

layer['in_height'] = input_shapes[0][2]
layer['in_width'] = input_shapes[0][3]
else:
output_shape = [
input_shapes[0][0], # Batch
input_shapes[0][1] - layer['crop_top'] - layer['crop_bottom'], # Height
input_shapes[0][2] - layer['crop_left'] - layer['crop_right'], # Width
input_shapes[0][3], # Channels
]
layer['out_height'] = output_shape[1]
layer['out_width'] = output_shape[2]
layer['n_chan'] = output_shape[3]

layer['in_height'] = input_shapes[0][1]
layer['in_width'] = input_shapes[0][2]

return layer, output_shape
43 changes: 43 additions & 0 deletions hls4ml/model/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,47 @@ def initialize(self):
self.add_output_variable(shape, dims, precision=inp.type.precision)


class Cropping1D(Layer):
_expected_attributes = [
Attribute('in_width'),
Attribute('out_width'),
Attribute('n_chan'),
Attribute('crop_left'),
Attribute('crop_right'),
]

def initialize(self):
inp = self.get_input_variable()
# no data_format attribute for Cropping1D
shape = [self.attributes['out_width'], self.attributes['n_chan']]
dims = [f'OUT_WIDTH_{self.index}', f'N_CHAN_{self.index}']
self.add_output_variable(shape, dims, precision=inp.type.precision)


class Cropping2D(Layer):
_expected_attributes = [
Attribute('in_height'),
Attribute('in_width'),
Attribute('out_height'),
Attribute('out_width'),
Attribute('n_chan'),
Attribute('crop_top'),
Attribute('crop_bottom'),
Attribute('crop_left'),
Attribute('crop_right'),
]

def initialize(self):
inp = self.get_input_variable()
if self.get_attr('data_format') == 'channels_last':
shape = [self.attributes['out_height'], self.attributes['out_width'], self.attributes['n_chan']]
dims = [f'OUT_HEIGHT_{self.index}', f'OUT_WIDTH_{self.index}', f'N_CHAN_{self.index}']
else:
shape = [self.attributes['n_chan'], self.attributes['out_height'], self.attributes['out_width']]
dims = [f'N_CHAN_{self.index}', f'OUT_HEIGHT_{self.index}', f'OUT_WIDTH_{self.index}']
self.add_output_variable(shape, dims, precision=inp.type.precision)


class Activation(Layer):
_expected_attributes = [
Attribute('n_in'),
Expand Down Expand Up @@ -1752,6 +1793,8 @@ def initialize(self):
'GlobalAveragePooling2D': GlobalPooling2D,
'ZeroPadding1D': ZeroPadding1D,
'ZeroPadding2D': ZeroPadding2D,
'Cropping1D': Cropping1D,
'Cropping2D': Cropping2D,
'Merge': Merge,
'MatMul': MatMul,
'Dot': Dot,
Expand Down
84 changes: 84 additions & 0 deletions hls4ml/templates/vivado/nnet_utils/nnet_cropping.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#ifndef NNET_CROPPING_H_
#define NNET_CROPPING_H_

#include <math.h>

namespace nnet {

struct cropping1d_config {
static const unsigned n_chan = 10;
static const unsigned in_width = 10;
static const unsigned out_width = 10;
static const unsigned crop_left = 0;
static const unsigned crop_right = 0;
};

// no need for channel first for 1D cropping (no keras equivalent)
template <class data_T, class res_T, typename CONFIG_T>
void cropping1d_cl(data_T data[CONFIG_T::n_chan * CONFIG_T::in_width], res_T res[CONFIG_T::n_chan * CONFIG_T::out_width]) {
#pragma HLS PIPELINE

// Skip cropped input from left
data += CONFIG_T::crop_left * CONFIG_T::n_chan;

// Fill upto out_width (implicit cropping from right)
for (int i = 0; i < CONFIG_T::out_width; i++) {
for (int j = 0; j < CONFIG_T::n_chan; j++) {
*(res++) = (res_T) * (data++);
}
}
}

struct cropping2d_config {
static const unsigned n_chan = 10;
static const unsigned in_height = 10;
static const unsigned in_width = 10;
static const unsigned out_height = 10;
static const unsigned out_width = 10;
static const unsigned crop_top = 0;
static const unsigned crop_bottom = 0;
static const unsigned crop_left = 0;
static const unsigned crop_right = 0;
};

template <class data_T, class res_T, typename CONFIG_T>
void cropping2d_cf(data_T data[CONFIG_T::n_chan * CONFIG_T::in_height * CONFIG_T::in_width],
res_T res[CONFIG_T::n_chan * CONFIG_T::out_height * CONFIG_T::out_width]) {
#pragma HLS PIPELINE

for (int k = 0; k < CONFIG_T::n_chan; k++) { // channels first
// Skip current channel data from top and left
data_T *data_ptr = data + k * CONFIG_T::in_height * CONFIG_T::in_width + CONFIG_T::crop_top * CONFIG_T::in_width +
CONFIG_T::crop_left;

// Fill upto out_height and out_width
for (int i = 0; i < CONFIG_T::out_height; i++) {
data_T *row_ptr = data_ptr + i * CONFIG_T::in_width;
for (int j = 0; j < CONFIG_T::out_width; j++) {
*(res++) = (res_T) * (row_ptr++);
}
}
}
}

template <class data_T, class res_T, typename CONFIG_T>
void cropping2d_cl(data_T data[CONFIG_T::n_chan * CONFIG_T::in_height * CONFIG_T::in_width],
res_T res[CONFIG_T::n_chan * CONFIG_T::out_height * CONFIG_T::out_width]) {
#pragma HLS PIPELINE

for (int i = 0; i < CONFIG_T::out_height; i++) {
int in_row = i + CONFIG_T::crop_top;
for (int j = 0; j < CONFIG_T::out_width; j++) {
int in_col = j + CONFIG_T::crop_left;

data_T *data_ptr = data + (in_row * CONFIG_T::in_width + in_col) * CONFIG_T::n_chan;
for (int k = 0; k < CONFIG_T::n_chan; k++) { // channels last
*(res++) = (res_T) * (data_ptr++);
}
}
}
}

} // namespace nnet

#endif
65 changes: 65 additions & 0 deletions hls4ml/templates/vivado/nnet_utils/nnet_cropping_stream.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#ifndef NNET_CROPPING_STREAM_H_
#define NNET_CROPPING_STREAM_H_

#include "nnet_padding_stream.h" // fill_data function
#include <math.h>

namespace nnet {

template <class data_T, class res_T, typename CONFIG_T>
void cropping1d_cl(hls::stream<data_T> &data, hls::stream<res_T> &res) {
#pragma HLS PIPELINE

// Discard left
for (int i = 0; i < CONFIG_T::crop_left; i++) {
data.read();
}

for (int i = 0; i < CONFIG_T::out_width; i++) {
fill_data<data_T, res_T, CONFIG_T>(data, res);
}

// Discard right
for (int i = 0; i < CONFIG_T::crop_right; i++) {
data.read();
}
}

template <class data_T, class res_T, typename CONFIG_T>
void cropping2d_cl(hls::stream<data_T> &data, hls::stream<res_T> &res) {
#pragma HLS PIPELINE

// Discard top rows
for (int i = 0; i < CONFIG_T::crop_top; i++) {
for (int j = 0; j < CONFIG_T::in_width; j++) {
data.read();
}
}

for (int i = 0; i < CONFIG_T::out_height; i++) {
// Discard left columns
for (int j = 0; j < CONFIG_T::crop_left; j++) {
data.read();
}

for (int j = 0; j < CONFIG_T::out_width; j++) {
fill_data<data_T, res_T, CONFIG_T>(data, res);
}

// Discard right columns
for (int j = 0; j < CONFIG_T::crop_right; j++) {
data.read();
}
}

// Discard bottom rows
for (int i = 0; i < CONFIG_T::crop_bottom; i++) {
for (int j = 0; j < CONFIG_T::in_width; j++) {
data.read();
}
}
}

} // namespace nnet

#endif
Loading
Loading